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::UnitLength;
9use kcl_error::CompilationIssue;
10use kcl_error::SourceRange;
11use serde::Serialize;
12use uuid::Uuid;
13
14use crate::ExecOutcome;
15use crate::ExecutorContext;
16use crate::KclError;
17use crate::KclErrorWithOutputs;
18use crate::KclValueView;
19use crate::Program;
20use crate::SegmentDragAnchor;
21use crate::collections::AhashIndexSet;
22use crate::execution::Artifact;
23use crate::execution::ArtifactGraph;
24use crate::execution::ArtifactId;
25use crate::execution::CapSubType;
26use crate::execution::CodeRef;
27use crate::execution::MockConfig;
28use crate::execution::SKETCH_BLOCK_PARAM_ON;
29use crate::execution::annotations::WarningLevel;
30use crate::execution::cache::SketchModeState;
31use crate::execution::cache::clear_mem_cache;
32use crate::execution::cache::read_old_memory;
33use crate::execution::cache::write_old_memory;
34use crate::execution::types::adjust_length;
35use crate::fmt::format_number_literal;
36use crate::front::Angle;
37use crate::front::ArcCtor;
38use crate::front::ArcDirection;
39use crate::front::CircleCtor;
40use crate::front::ControlPointSplineCtor;
41use crate::front::Distance;
42use crate::front::EqualRadius;
43use crate::front::Error;
44use crate::front::ExecResult;
45use crate::front::FixedPoint;
46use crate::front::Freedom;
47use crate::front::LinesEqualLength;
48use crate::front::Midpoint;
49use crate::front::Object;
50use crate::front::Parallel;
51use crate::front::Perpendicular;
52use crate::front::PointCtor;
53use crate::front::Symmetric;
54use crate::front::Tangent;
55use crate::frontend::api::CapSource;
56use crate::frontend::api::Expr;
57use crate::frontend::api::FileId;
58use crate::frontend::api::Number;
59use crate::frontend::api::ObjectId;
60use crate::frontend::api::ObjectKind;
61use crate::frontend::api::Plane;
62use crate::frontend::api::ProjectId;
63use crate::frontend::api::RestoreSketchCheckpointOutcome;
64use crate::frontend::api::SceneGraph;
65use crate::frontend::api::SceneGraphDelta;
66use crate::frontend::api::SketchCheckpointId;
67use crate::frontend::api::SourceDelta;
68use crate::frontend::api::SourceRef;
69use crate::frontend::api::SourceRefRange;
70use crate::frontend::api::Version;
71use crate::frontend::api::WallSource;
72use crate::frontend::modify::find_defined_names;
73use crate::frontend::modify::next_free_name;
74use crate::frontend::modify::next_free_name_with_padding;
75use crate::frontend::sketch::Coincident;
76use crate::frontend::sketch::Constraint;
77use crate::frontend::sketch::ConstraintLabelPositionEdit;
78use crate::frontend::sketch::ConstraintSegment;
79use crate::frontend::sketch::Diameter;
80use crate::frontend::sketch::ExistingSegmentCtor;
81use crate::frontend::sketch::Horizontal;
82use crate::frontend::sketch::LineCtor;
83use crate::frontend::sketch::Point2d;
84use crate::frontend::sketch::Radius;
85use crate::frontend::sketch::Segment;
86use crate::frontend::sketch::SegmentCtor;
87use crate::frontend::sketch::SketchApi;
88use crate::frontend::sketch::SketchCtor;
89use crate::frontend::sketch::Vertical;
90use crate::id::IncIdGenerator;
91use crate::parsing::ast::types as ast;
92use crate::parsing::ast::types::BoxNode;
93use crate::parsing::ast::types::CallExpressionKw;
94use crate::parsing::ast::types::NodePathExt;
95use crate::pretty::NumericSuffix;
96use crate::std::constraints::LinesAtAngleKind;
97use crate::walk::NodeMut;
98use crate::walk::Visitable;
99use crate::walk::traverse::MutateBodyItem;
100use crate::walk::traverse::TraversalReturn;
101use crate::walk::traverse::Visitor;
102use crate::walk::traverse::dfs_mut;
103
104pub(crate) mod api;
105pub(crate) mod modify;
106pub(crate) mod sketch;
107
108pub const MAX_SKETCH_CHECKPOINTS: usize = 100;
109
110#[derive(Debug, Clone)]
111struct SketchCheckpoint {
112    id: SketchCheckpointId,
113    source: SourceDelta,
114    program: Program,
115    scene_graph: SceneGraph,
116    exec_outcome: ExecOutcome,
117    point_freedom_cache: HashMap<ObjectId, Freedom>,
118    mock_memory: Option<SketchModeState>,
119}
120pub(crate) mod trim;
121
122struct ArcSizeConstraintParams {
123    points: Vec<ObjectId>,
124    function_name: &'static str,
125    value: f64,
126    units: NumericSuffix,
127    label_position: Option<Point2d<Number>>,
128    constraint_type_name: &'static str,
129}
130
131const POINT_FN: &str = "point";
132const POINT_AT_PARAM: &str = "at";
133const LINE_FN: &str = "line";
134const LINE_VARIABLE: &str = "line";
135const LINE_START_PARAM: &str = "start";
136const LINE_END_PARAM: &str = "end";
137const ARC_FN: &str = "arc";
138const ARC_VARIABLE: &str = "arc";
139const ARC_START_PARAM: &str = "start";
140const ARC_END_PARAM: &str = "end";
141const ARC_CENTER_PARAM: &str = "center";
142const ARC_DIRECTION_PARAM: &str = "direction";
143/// The name of the KCL std constant for a clockwise arc direction.
144const ARC_DIRECTION_CW_NAME: &str = "CW";
145const CIRCLE_FN: &str = "circle";
146const CIRCLE_VARIABLE: &str = "circle";
147const CIRCLE_START_PARAM: &str = "start";
148const CIRCLE_CENTER_PARAM: &str = "center";
149const CONTROL_POINT_SPLINE_FN: &str = "controlPointSpline";
150const CONTROL_POINT_SPLINE_POINTS_PARAM: &str = "points";
151const LABEL_POSITION_PARAM: &str = "labelPosition";
152
153const COINCIDENT_FN: &str = "coincident";
154const DIAMETER_FN: &str = "diameter";
155const DISTANCE_FN: &str = "distance";
156const FIXED_FN: &str = "fixed";
157const ANGLE_FN: &str = "angle";
158const ANGLE_DIMENSION_FN: &str = "angleDimension";
159const ANGLE_LINES_PARAM: &str = "lines";
160const ANGLE_SECTOR_PARAM: &str = "sector";
161const ANGLE_INVERSE_PARAM: &str = "inverse";
162const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
163const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
164const EQUAL_LENGTH_FN: &str = "equalLength";
165const EQUAL_RADIUS_FN: &str = "equalRadius";
166const HORIZONTAL_FN: &str = "horizontal";
167const MIDPOINT_FN: &str = "midpoint";
168const MIDPOINT_POINT_PARAM: &str = "point";
169const RADIUS_FN: &str = "radius";
170const SYMMETRIC_FN: &str = "symmetric";
171const SYMMETRIC_AXIS_PARAM: &str = "axis";
172const TANGENT_FN: &str = "tangent";
173const VERTICAL_FN: &str = "vertical";
174
175const LINE_PROPERTY_START: &str = "start";
176const LINE_PROPERTY_END: &str = "end";
177
178const ARC_PROPERTY_START: &str = "start";
179const ARC_PROPERTY_END: &str = "end";
180const ARC_PROPERTY_CENTER: &str = "center";
181const CIRCLE_PROPERTY_START: &str = "start";
182const CIRCLE_PROPERTY_CENTER: &str = "center";
183const CONTROL_POINT_SPLINE_PROPERTY_CONTROLS: &str = "controls";
184const CONTROL_POINT_SPLINE_PROPERTY_EDGES: &str = "edges";
185
186const CONSTRUCTION_PARAM: &str = "construction";
187
188#[derive(Debug, Clone, Copy)]
189enum EditDeleteKind {
190    Edit,
191    DeleteNonSketch,
192}
193
194/// Options that control how an edit is re-executed and written back.
195struct ExecuteAfterEditOptions {
196    segment_ids_edited: AhashIndexSet<ObjectId>,
197    edit_kind: EditDeleteKind,
198    commit_solved_initial_guesses: bool,
199}
200
201impl EditDeleteKind {
202    /// Returns true if this edit is any type of deletion.
203    fn is_delete(&self) -> bool {
204        match self {
205            EditDeleteKind::Edit => false,
206            EditDeleteKind::DeleteNonSketch => true,
207        }
208    }
209
210    fn to_change_kind(self) -> ChangeKind {
211        match self {
212            EditDeleteKind::Edit => ChangeKind::Edit,
213            EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
214        }
215    }
216}
217
218#[derive(Debug, Clone, Copy)]
219enum ChangeKind {
220    Add,
221    Edit,
222    Delete,
223    None,
224}
225
226#[derive(Debug, Clone, Serialize, ts_rs::TS)]
227#[ts(export, export_to = "FrontendApi.ts")]
228#[serde(tag = "type")]
229pub enum SetProgramOutcome {
230    #[serde(rename_all = "camelCase")]
231    Success {
232        scene_graph: Box<SceneGraph>,
233        exec_outcome: Box<ExecOutcome>,
234        checkpoint_id: Option<SketchCheckpointId>,
235    },
236    #[serde(rename_all = "camelCase")]
237    ExecFailure { error: Box<KclErrorWithOutputs> },
238}
239
240/// Options for a sketch segment edit that participates in drag solving.
241pub struct EditSegmentsOptions {
242    /// Narrows which edited scene objects receive temporary fixed constraints.
243    ///
244    /// `None` keeps the default of anchoring every edited segment. `Some(vec![])`
245    /// disables those fixed constraints, which is useful for semantic edits such
246    /// as toggling construction state.
247    pub anchor_segment_ids: Option<Vec<ObjectId>>,
248    /// Hidden fixed cursor points that the referenced segment bodies must pass
249    /// through during solve.
250    pub drag_anchors: Vec<SegmentDragAnchor>,
251    /// Constraint label positions to write in the same AST transaction as the
252    /// segment edits. Label positions do not participate in solving.
253    pub constraint_label_edits: Vec<ConstraintLabelPositionEdit>,
254    /// Whether solver-updated initial guesses should be written back to KCL.
255    pub commit_solved_initial_guesses: bool,
256}
257
258/// Options for a distance-constraint label edit during sketch dragging.
259pub struct EditDistanceConstraintLabelPositionOptions {
260    /// Edited scene objects to keep anchored while previewing the label edit.
261    pub anchor_segment_ids: Vec<ObjectId>,
262    /// Whether solver-updated initial guesses should be written back to KCL.
263    pub commit_solved_initial_guesses: bool,
264}
265
266/// Options for editing a constraint during sketch dragging.
267pub struct EditConstraintOptions {
268    /// Whether solver-updated initial guesses should be written back to KCL.
269    pub commit_solved_initial_guesses: bool,
270}
271
272#[derive(Debug, Clone)]
273struct SolidAstReference {
274    variable_name: String,
275    output_index: Option<usize>,
276}
277
278#[derive(Debug, Clone)]
279pub struct FrontendState {
280    program: Program,
281    scene_graph: SceneGraph,
282    /// Lightweight map from engine solid IDs to their latest KCL references.
283    solid_references: HashMap<Uuid, SolidAstReference>,
284    /// Stores the last known freedom value for each point object.
285    /// This allows us to preserve freedom values when freedom analysis isn't run.
286    point_freedom_cache: HashMap<ObjectId, Freedom>,
287    /// One-shot drag anchors for the next segment edit. These ids define which
288    /// edited points/segments become temporary fixed constraints during solve.
289    next_drag_anchor_segment_ids: Option<AhashIndexSet<ObjectId>>,
290    /// One-shot segment-body drag anchors for the next segment edit. These add
291    /// a temporary solver point on the dragged segment that follows the cursor.
292    next_segment_drag_anchors: Option<Vec<SegmentDragAnchor>>,
293    /// One-shot constraint label edits to apply with the next segment edit.
294    next_constraint_label_edits: Option<Vec<ConstraintLabelPositionEdit>>,
295    /// One-shot override for whether the next edit commits solver-updated
296    /// initial guesses back into KCL. Drag previews keep this off so only the
297    /// explicit drag edit feeds the next solve.
298    next_edit_commits_solver_solutions: Option<bool>,
299    sketch_checkpoints: VecDeque<SketchCheckpoint>,
300    sketch_checkpoint_id_gen: IncIdGenerator<u64>,
301}
302
303impl Default for FrontendState {
304    fn default() -> Self {
305        Self::new()
306    }
307}
308
309impl FrontendState {
310    pub fn new() -> Self {
311        Self {
312            program: Program::empty(),
313            scene_graph: SceneGraph {
314                project: ProjectId(0),
315                file: FileId(0),
316                version: Version(0),
317                objects: Default::default(),
318                settings: Default::default(),
319                sketch_mode: Default::default(),
320            },
321            solid_references: HashMap::new(),
322            point_freedom_cache: HashMap::new(),
323            next_drag_anchor_segment_ids: None,
324            next_segment_drag_anchors: None,
325            next_constraint_label_edits: None,
326            next_edit_commits_solver_solutions: None,
327            sketch_checkpoints: VecDeque::new(),
328            sketch_checkpoint_id_gen: IncIdGenerator::new(1),
329        }
330    }
331
332    /// Get a reference to the scene graph
333    pub fn scene_graph(&self) -> &SceneGraph {
334        &self.scene_graph
335    }
336
337    pub fn default_length_unit(&self) -> UnitLength {
338        self.program
339            .meta_settings()
340            .ok()
341            .flatten()
342            .map(|settings| settings.default_length_units)
343            .unwrap_or(UnitLength::Millimeters)
344    }
345
346    pub async fn create_sketch_checkpoint(&mut self, exec_outcome: ExecOutcome) -> api::Result<SketchCheckpointId> {
347        let checkpoint_id = SketchCheckpointId::new(self.sketch_checkpoint_id_gen.next_id());
348
349        let checkpoint = SketchCheckpoint {
350            id: checkpoint_id,
351            source: SourceDelta {
352                text: source_from_ast(&self.program.ast),
353            },
354            program: self.program.clone(),
355            scene_graph: self.scene_graph.clone(),
356            exec_outcome,
357            point_freedom_cache: self.point_freedom_cache.clone(),
358            mock_memory: read_old_memory().await,
359        };
360
361        self.sketch_checkpoints.push_back(checkpoint);
362        while self.sketch_checkpoints.len() > MAX_SKETCH_CHECKPOINTS {
363            self.sketch_checkpoints.pop_front();
364        }
365
366        Ok(checkpoint_id)
367    }
368
369    /// Edit sketch segments with optional drag-solve overrides.
370    ///
371    /// Drag anchors add hidden fixed cursor points and constrain the referenced
372    /// segment bodies to pass through them, which lets body drags use the same
373    /// anchor model without pinning all child points. Preview callers disable
374    /// solver writeback so solved geometry can be returned without feeding every
375    /// solver value back into KCL.
376    pub async fn edit_segments_with_options(
377        &mut self,
378        ctx: &ExecutorContext,
379        version: Version,
380        sketch: ObjectId,
381        segments: Vec<ExistingSegmentCtor>,
382        options: EditSegmentsOptions,
383    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
384        let previous_anchor_ids = options.anchor_segment_ids.map(|anchor_ids| {
385            self.next_drag_anchor_segment_ids
386                .replace(anchor_ids.into_iter().collect())
387        });
388        let previous_drag_anchors = self.next_segment_drag_anchors.replace(options.drag_anchors);
389        let previous_constraint_label_edits = self.next_constraint_label_edits.replace(options.constraint_label_edits);
390        let previous_commit_mode = self
391            .next_edit_commits_solver_solutions
392            .replace(options.commit_solved_initial_guesses);
393        let result = SketchApi::edit_segments(self, ctx, version, sketch, segments).await;
394        if let Some(previous_anchor_ids) = previous_anchor_ids {
395            self.next_drag_anchor_segment_ids = previous_anchor_ids;
396        }
397        self.next_segment_drag_anchors = previous_drag_anchors;
398        self.next_constraint_label_edits = previous_constraint_label_edits;
399        self.next_edit_commits_solver_solutions = previous_commit_mode;
400        result
401    }
402
403    /// Edit a distance-constraint label position with optional solver writeback.
404    ///
405    /// Drag previews set `commit_solved_initial_guesses` to false so label
406    /// placement can be previewed against solved geometry without advancing
407    /// persistent KCL state until drag completion.
408    pub async fn edit_distance_constraint_label_position_with_options(
409        &mut self,
410        ctx: &ExecutorContext,
411        version: Version,
412        sketch: ObjectId,
413        constraint_id: ObjectId,
414        label_position: Point2d<Number>,
415        options: EditDistanceConstraintLabelPositionOptions,
416    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
417        let previous_commit_mode = self
418            .next_edit_commits_solver_solutions
419            .replace(options.commit_solved_initial_guesses);
420        let result = SketchApi::edit_distance_constraint_label_position(
421            self,
422            ctx,
423            version,
424            sketch,
425            constraint_id,
426            label_position,
427            options.anchor_segment_ids,
428        )
429        .await;
430        self.next_edit_commits_solver_solutions = previous_commit_mode;
431        result
432    }
433
434    /// Edit a distance constraint with optional solver writeback.
435    pub async fn edit_distance_constraint_with_options(
436        &mut self,
437        ctx: &ExecutorContext,
438        version: Version,
439        sketch: ObjectId,
440        constraint_id: ObjectId,
441        constraint: Constraint,
442        options: EditConstraintOptions,
443    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
444        self.edit_constraint_with_options(ctx, version, sketch, constraint_id, constraint, options)
445            .await
446    }
447
448    /// Edit an angle constraint with optional solver writeback.
449    pub async fn edit_angle_constraint_with_options(
450        &mut self,
451        ctx: &ExecutorContext,
452        version: Version,
453        sketch: ObjectId,
454        constraint_id: ObjectId,
455        angle: Angle,
456        options: EditConstraintOptions,
457    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
458        self.edit_constraint_with_options(ctx, version, sketch, constraint_id, Constraint::Angle(angle), options)
459            .await
460    }
461
462    /// Edit an angle or distance-family constraint with optional solver writeback.
463    pub async fn edit_constraint_with_options(
464        &mut self,
465        ctx: &ExecutorContext,
466        _version: Version,
467        sketch: ObjectId,
468        constraint_id: ObjectId,
469        constraint: Constraint,
470        options: EditConstraintOptions,
471    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
472        // TODO: Check version.
473        let sketch_block_ref =
474            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
475
476        let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
477            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
478        })?;
479
480        let mut new_ast = self.program.ast.clone();
481        let command = match &object.kind {
482            ObjectKind::Constraint {
483                constraint:
484                    Constraint::Distance(_) | Constraint::HorizontalDistance(_) | Constraint::VerticalDistance(_),
485            } => {
486                let (function_name, distance) = match &constraint {
487                    Constraint::Distance(distance) => (DISTANCE_FN, distance),
488                    Constraint::HorizontalDistance(distance) => (HORIZONTAL_DISTANCE_FN, distance),
489                    Constraint::VerticalDistance(distance) => (VERTICAL_DISTANCE_FN, distance),
490                    _ => {
491                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
492                            "A distance constraint can only be replaced by another distance constraint".to_owned(),
493                        )));
494                    }
495                };
496                let (call, value) = self
497                    .distance_constraint_ast_parts(function_name, distance, &mut new_ast)
498                    .map_err(KclErrorWithOutputs::no_outputs)?;
499                AstMutateCommand::EditDistanceConstraint { call, value }
500            }
501            ObjectKind::Constraint {
502                constraint: Constraint::Angle(_),
503            } => {
504                let Constraint::Angle(angle) = &constraint else {
505                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
506                        "An angle constraint can only be replaced by another angle constraint".to_owned(),
507                    )));
508                };
509                let (call, value) = self
510                    .angle_constraint_ast_parts(angle, &mut new_ast)
511                    .map_err(KclErrorWithOutputs::no_outputs)?;
512                AstMutateCommand::EditAngleConstraint { call, value }
513            }
514            ObjectKind::Constraint { .. } => {
515                return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
516                    "Editing {} is not supported",
517                    object.kind.human_friendly_kind_with_article(),
518                ))));
519            }
520            _ => {
521                return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
522                    "Object is not a constraint: {constraint_id:?}"
523                ))));
524            }
525        };
526
527        self.mutate_ast(&mut new_ast, constraint_id, command)
528            .map_err(KclErrorWithOutputs::no_outputs)?;
529
530        self.execute_after_edit(
531            ctx,
532            sketch,
533            sketch_block_ref,
534            &mut new_ast,
535            ExecuteAfterEditOptions {
536                segment_ids_edited: Default::default(),
537                edit_kind: EditDeleteKind::Edit,
538                commit_solved_initial_guesses: options.commit_solved_initial_guesses,
539            },
540        )
541        .await
542    }
543
544    pub async fn restore_sketch_checkpoint(
545        &mut self,
546        checkpoint_id: SketchCheckpointId,
547    ) -> api::Result<RestoreSketchCheckpointOutcome> {
548        let checkpoint = self
549            .sketch_checkpoints
550            .iter()
551            .find(|checkpoint| checkpoint.id == checkpoint_id)
552            .cloned()
553            .ok_or_else(|| Error {
554                msg: format!("Sketch checkpoint not found: {checkpoint_id:?}"),
555            })?;
556
557        self.program = checkpoint.program;
558        self.scene_graph = checkpoint.scene_graph.clone();
559        self.solid_references = solid_references_from_variables(&self.program.ast, &checkpoint.exec_outcome.variables);
560        self.point_freedom_cache = checkpoint.point_freedom_cache;
561        self.next_drag_anchor_segment_ids = None;
562        self.next_segment_drag_anchors = None;
563        self.next_constraint_label_edits = None;
564        self.next_edit_commits_solver_solutions = None;
565
566        if let Some(mock_memory) = checkpoint.mock_memory {
567            write_old_memory(mock_memory).await;
568        } else {
569            clear_mem_cache().await;
570        }
571
572        Ok(RestoreSketchCheckpointOutcome {
573            source_delta: checkpoint.source,
574            scene_graph_delta: SceneGraphDelta {
575                new_graph: self.scene_graph_for_ui(),
576                new_objects: Vec::new(),
577                invalidates_ids: true,
578                exec_outcome: checkpoint.exec_outcome,
579            },
580        })
581    }
582
583    pub fn clear_sketch_checkpoints(&mut self) {
584        self.sketch_checkpoints.clear();
585    }
586    fn scene_graph_for_ui(&self) -> SceneGraph {
587        let has_control_point_splines = self.scene_graph.objects.iter().any(|object| {
588            matches!(
589                object.kind,
590                ObjectKind::Segment {
591                    segment: Segment::ControlPointSpline(_)
592                }
593            )
594        });
595
596        if !has_control_point_splines {
597            return self.scene_graph.clone();
598        }
599
600        let hidden_constraint_ids = self
601            .scene_graph
602            .objects
603            .iter()
604            .filter_map(|object| match &object.kind {
605                ObjectKind::Constraint {
606                    constraint: Constraint::Coincident(coincident),
607                } if coincident_is_internal_to_same_control_point_spline(coincident, &self.scene_graph) => {
608                    Some(object.id)
609                }
610                _ => None,
611            })
612            .collect::<HashSet<_>>();
613
614        if hidden_constraint_ids.is_empty() {
615            return self.scene_graph.clone();
616        }
617
618        let mut scene_graph = self.scene_graph.clone();
619        for object in &mut scene_graph.objects {
620            match &mut object.kind {
621                ObjectKind::Constraint { .. } if hidden_constraint_ids.contains(&object.id) => {
622                    object.kind = ObjectKind::Nil;
623                }
624                ObjectKind::Sketch(sketch) => {
625                    sketch
626                        .constraints
627                        .retain(|constraint_id| !hidden_constraint_ids.contains(constraint_id));
628                }
629                _ => {}
630            }
631        }
632
633        scene_graph
634    }
635}
636
637fn coincident_is_internal_to_same_control_point_spline(coincident: &Coincident, scene_graph: &SceneGraph) -> bool {
638    let mut first_owner_id = None;
639    for segment_id in coincident.segment_ids() {
640        let Some(owner_id) = owning_control_point_spline_id(segment_id, scene_graph) else {
641            return false;
642        };
643
644        match first_owner_id {
645            Some(first_owner_id) if first_owner_id != owner_id => return false,
646            Some(_) => {}
647            None => first_owner_id = Some(owner_id),
648        }
649    }
650
651    first_owner_id.is_some()
652}
653
654fn owning_control_point_spline_id(segment_id: ObjectId, scene_graph: &SceneGraph) -> Option<ObjectId> {
655    let object = scene_graph.objects.get(segment_id.0)?;
656    let ObjectKind::Segment { segment } = &object.kind else {
657        return None;
658    };
659
660    match segment {
661        Segment::ControlPointSpline(_) => Some(segment_id),
662        Segment::Point(point) => point
663            .owner
664            .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
665        Segment::Line(line) => line
666            .owner
667            .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
668        _ => None,
669    }
670}
671
672fn matches_control_point_spline_owner(owner_id: ObjectId, scene_graph: &SceneGraph) -> bool {
673    matches!(
674        scene_graph.objects.get(owner_id.0).map(|object| &object.kind),
675        Some(ObjectKind::Segment {
676            segment: Segment::ControlPointSpline(_)
677        })
678    )
679}
680
681fn ensure_control_point_spline_experimental_features(program: &Program) -> Result<Program, KclError> {
682    let experimental_features_allowed = program
683        .meta_settings()
684        .ok()
685        .flatten()
686        .map(|settings| settings.experimental_features == WarningLevel::Allow)
687        .unwrap_or(false);
688    if experimental_features_allowed {
689        return Ok(program.clone());
690    }
691
692    program.change_experimental_features(Some(WarningLevel::Allow))
693}
694
695impl SketchApi for FrontendState {
696    async fn execute_mock(
697        &mut self,
698        ctx: &ExecutorContext,
699        _version: Version,
700        sketch: ObjectId,
701    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
702        let sketch_block_ref =
703            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
704
705        let mut truncated_program = self.program.clone();
706        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
707            .map_err(KclErrorWithOutputs::no_outputs)?;
708
709        // Execute.
710        let outcome = ctx
711            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
712            .await?;
713        let new_source = source_from_ast(&self.program.ast);
714        let src_delta = SourceDelta { text: new_source };
715        // MockConfig::default() has freedom_analysis: true
716        let outcome = self.update_state_after_exec(outcome, true);
717        let scene_graph_delta = SceneGraphDelta {
718            new_graph: self.scene_graph.clone(),
719            new_objects: Default::default(),
720            invalidates_ids: false,
721            exec_outcome: outcome,
722        };
723        Ok((src_delta, scene_graph_delta))
724    }
725
726    async fn new_sketch(
727        &mut self,
728        ctx: &ExecutorContext,
729        _project: ProjectId,
730        _file: FileId,
731        _version: Version,
732        args: SketchCtor,
733    ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)> {
734        // TODO: Check version.
735
736        let mut new_ast = self.program.ast.clone();
737        // Create updated KCL source from args.
738        let mut plane_ast = sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &self.solid_references, &args.on)
739            .map_err(KclErrorWithOutputs::no_outputs)?;
740        let mut defined_names = find_defined_names(&new_ast);
741        let is_face_of_expr = matches!(
742            &plane_ast,
743            ast::Expr::CallExpressionKw(call) if call.callee.name.name == "faceOf"
744        );
745        if is_face_of_expr {
746            let face_name = next_free_name_with_padding("face", &defined_names)
747                .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
748            let face_decl = ast::VariableDeclaration::new(
749                ast::VariableDeclarator::new(&face_name, plane_ast),
750                ast::ItemVisibility::Default,
751                ast::VariableKind::Const,
752            );
753            new_ast
754                .body
755                .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
756                    face_decl,
757                ))));
758            defined_names.insert(face_name.clone());
759            plane_ast = ast::Expr::Name(BoxNode::new(ast::Name::new(&face_name)));
760        }
761        let sketch_ast = ast::SketchBlock {
762            arguments: vec![ast::LabeledArg {
763                label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
764                arg: plane_ast,
765            }],
766            body: Default::default(),
767            is_being_edited: false,
768            non_code_meta: Default::default(),
769            digest: None,
770        };
771        // Add a sketch block as a variable declaration directly, avoiding
772        // source-range mutation on a no-src node.
773        let sketch_name = next_free_name_with_padding("sketch", &defined_names)
774            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
775        let sketch_decl = ast::VariableDeclaration::new(
776            ast::VariableDeclarator::new(
777                &sketch_name,
778                ast::Expr::SketchBlock(BoxNode::new(ast::Node::no_src(sketch_ast))),
779            ),
780            ast::ItemVisibility::Default,
781            ast::VariableKind::Const,
782        );
783        new_ast
784            .body
785            .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
786                sketch_decl,
787            ))));
788        // Convert to string source to create real source ranges.
789        let new_source = source_from_ast(&new_ast);
790        // Parse the new source.
791        let new_program = parse_frontend_mutation_source(
792            &new_source,
793            "Error parsing KCL source after adding sketch",
794            "No AST produced after adding sketch",
795        )?;
796
797        // Make sure to only set this if there are no errors.
798        self.program = new_program.clone();
799
800        // We need to do an engine execute so that the plane object gets created
801        // and is cached.
802        let outcome = ctx.run_with_caching(new_program.clone()).await?;
803        let freedom_analysis_ran = true;
804
805        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
806
807        let Some(sketch_id) = self
808            .scene_graph
809            .objects
810            .iter()
811            .filter_map(|object| match object.kind {
812                ObjectKind::Sketch(_) => Some(object.id),
813                _ => None,
814            })
815            .max_by_key(|id| id.0)
816        else {
817            return Err(KclErrorWithOutputs::from_error_outcome(
818                KclError::refactor("No objects in scene graph after adding sketch".to_owned()),
819                outcome,
820            ));
821        };
822        // Store the object in the scene.
823        self.scene_graph.sketch_mode = Some(sketch_id);
824
825        let src_delta = SourceDelta { text: new_source };
826        let scene_graph_delta = SceneGraphDelta {
827            new_graph: self.scene_graph_for_ui(),
828            invalidates_ids: false,
829            new_objects: vec![sketch_id],
830            exec_outcome: outcome,
831        };
832        Ok((src_delta, scene_graph_delta, sketch_id))
833    }
834
835    async fn edit_sketch(
836        &mut self,
837        ctx: &ExecutorContext,
838        _project: ProjectId,
839        _file: FileId,
840        _version: Version,
841        sketch: ObjectId,
842    ) -> ExecResult<SceneGraphDelta> {
843        // TODO: Check version.
844
845        // Look up existing sketch.
846        let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
847            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
848        })?;
849        let ObjectKind::Sketch(_) = &sketch_object.kind else {
850            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
851                "Object is not a sketch, it is {}",
852                sketch_object.kind.human_friendly_kind_with_article()
853            ))));
854        };
855        let sketch_block_ref = expect_single_node_ref(sketch_object).map_err(KclErrorWithOutputs::no_outputs)?;
856
857        // Enter sketch mode by setting the sketch_mode.
858        self.scene_graph.sketch_mode = Some(sketch);
859
860        // Truncate after the sketch block for mock execution.
861        let mut truncated_program = self.program.clone();
862        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
863            .map_err(KclErrorWithOutputs::no_outputs)?;
864
865        // Execute in mock mode to ensure state is up to date. The caller will
866        // want freedom analysis to display segments correctly.
867        let outcome = ctx
868            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
869            .await?;
870
871        // MockConfig::default() has freedom_analysis: true
872        let outcome = self.update_state_after_exec(outcome, true);
873        let scene_graph_delta = SceneGraphDelta {
874            new_graph: self.scene_graph_for_ui(),
875            invalidates_ids: false,
876            new_objects: Vec::new(),
877            exec_outcome: outcome,
878        };
879        Ok(scene_graph_delta)
880    }
881
882    async fn exit_sketch(
883        &mut self,
884        ctx: &ExecutorContext,
885        _version: Version,
886        sketch: ObjectId,
887    ) -> ExecResult<SceneGraph> {
888        // TODO: Check version.
889        #[cfg(not(target_arch = "wasm32"))]
890        let _ = sketch;
891        #[cfg(target_arch = "wasm32")]
892        if self.scene_graph.sketch_mode != Some(sketch) {
893            web_sys::console::warn_1(
894                &format!(
895                    "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
896                    self.scene_graph.sketch_mode
897                )
898                .into(),
899            );
900        }
901        self.scene_graph.sketch_mode = None;
902
903        // Execute.
904        let outcome = ctx.run_with_caching(self.program.clone()).await?;
905
906        // exit_sketch doesn't run freedom analysis, just clears sketch_mode
907        self.update_state_after_exec(outcome, false);
908
909        Ok(self.scene_graph_for_ui())
910    }
911
912    async fn delete_sketch(
913        &mut self,
914        ctx: &ExecutorContext,
915        _version: Version,
916        sketch: ObjectId,
917    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
918        // TODO: Check version.
919
920        let mut new_ast = self.program.ast.clone();
921
922        // Look up existing sketch.
923        let sketch_id = sketch;
924        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
925            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
926        })?;
927        let ObjectKind::Sketch(_) = &sketch_object.kind else {
928            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
929                "Object is not a sketch, it is {}",
930                sketch_object.kind.human_friendly_kind_with_article(),
931            ))));
932        };
933
934        // Modify the AST to remove the sketch.
935        self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)
936            .map_err(KclErrorWithOutputs::no_outputs)?;
937
938        self.execute_after_delete_sketch(ctx, &mut new_ast).await
939    }
940
941    async fn add_segment(
942        &mut self,
943        ctx: &ExecutorContext,
944        _version: Version,
945        sketch: ObjectId,
946        segment: SegmentCtor,
947        _label: Option<String>,
948    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
949        // TODO: Check version.
950        match segment {
951            SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
952            SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
953            SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
954            SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
955            SegmentCtor::ControlPointSpline(ctor) => self.add_control_point_spline(ctx, sketch, ctor).await,
956        }
957    }
958
959    async fn edit_segments(
960        &mut self,
961        ctx: &ExecutorContext,
962        _version: Version,
963        sketch: ObjectId,
964        segments: Vec<ExistingSegmentCtor>,
965    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
966        // TODO: Check version.
967        let sketch_block_ref =
968            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
969
970        let mut new_ast = self.program.ast.clone();
971        let mut edited_segment_ids = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
972        let mut invalidates_ids = false;
973
974        // edited_segment_ids still has to be the original segments (not final_edits), otherwise the owner segments
975        // are passed to `execute_after_edit` which changes the result of the solver, causing tests to fail.
976        for segment in &segments {
977            edited_segment_ids.insert(segment.id);
978            if let SegmentCtor::ControlPointSpline(new_ctor) = &segment.ctor
979                && let Some(existing_object) = self.scene_graph.objects.get(segment.id.0)
980                && let ObjectKind::Segment {
981                    segment: Segment::ControlPointSpline(existing_spline),
982                } = &existing_object.kind
983                && existing_spline.controls.len() != new_ctor.points.len()
984            {
985                invalidates_ids = true;
986            }
987        }
988        let drag_anchor_segment_ids = self
989            .next_drag_anchor_segment_ids
990            .take()
991            .unwrap_or_else(|| edited_segment_ids.clone());
992        let constraint_label_edits = self.next_constraint_label_edits.take().unwrap_or_default();
993        let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
994
995        // Preprocess segments into a final_edits vector to handle if segments contains:
996        // - edit start point of line1 (as SegmentCtor::Point)
997        // - edit end point of line1 (as SegmentCtor::Point)
998        //
999        // This would result in only the end point to be updated because edit_point() clones line1's ctor from
1000        // scene_graph, but this is still the old ctor because self.scene_graph is only updated after the loop finishes.
1001        //
1002        // To fix this, and other cases when the same point is edited from multiple elements in the segments Vec
1003        // we apply all edits in order to final_edits in a way that owned point edits result in line edits,
1004        // so the above example would result in a single line1 edit:
1005        // - the first start point edit creates a new line edit entry in final_edits
1006        // - the second end point edit finds this line edit and mutates the end position only.
1007        //
1008        // The result is that segments are flattened into a single IndexMap of edits by their owners, later edits overriding earlier ones.
1009        let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
1010
1011        for segment in segments {
1012            let segment_id = segment.id;
1013            match segment.ctor {
1014                SegmentCtor::Point(ctor) => {
1015                    // Find the owner, if any (point -> line / arc)
1016                    if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
1017                        && let ObjectKind::Segment { segment } = &segment_object.kind
1018                        && let Segment::Point(point) = segment
1019                        && let Some(owner_id) = point.owner
1020                        && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
1021                        && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
1022                    {
1023                        match owner_segment {
1024                            Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
1025                                if let Some(existing) = final_edits.get_mut(&owner_id) {
1026                                    let SegmentCtor::Line(line_ctor) = existing else {
1027                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1028                                            "Internal: Expected line ctor for owner, but found {}",
1029                                            existing.human_friendly_kind_with_article()
1030                                        ))));
1031                                    };
1032                                    // Line owner is already in final_edits -> apply this point edit
1033                                    if line.start == segment_id {
1034                                        line_ctor.start = ctor.position;
1035                                    } else {
1036                                        line_ctor.end = ctor.position;
1037                                    }
1038                                } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
1039                                    // Line owner is not in final_edits yet -> create it
1040                                    let mut line_ctor = line_ctor.clone();
1041                                    if line.start == segment_id {
1042                                        line_ctor.start = ctor.position;
1043                                    } else {
1044                                        line_ctor.end = ctor.position;
1045                                    }
1046                                    final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
1047                                } else {
1048                                    // This should never run..
1049                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1050                                        "Internal: Line does not have line ctor, but found {}",
1051                                        line.ctor.human_friendly_kind_with_article()
1052                                    ))));
1053                                }
1054                                continue;
1055                            }
1056                            Segment::Arc(arc)
1057                                if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
1058                            {
1059                                if let Some(existing) = final_edits.get_mut(&owner_id) {
1060                                    let SegmentCtor::Arc(arc_ctor) = existing else {
1061                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1062                                            "Internal: Expected arc ctor for owner, but found {}",
1063                                            existing.human_friendly_kind_with_article()
1064                                        ))));
1065                                    };
1066                                    if arc.start == segment_id {
1067                                        arc_ctor.start = ctor.position;
1068                                    } else if arc.end == segment_id {
1069                                        arc_ctor.end = ctor.position;
1070                                    } else {
1071                                        arc_ctor.center = ctor.position;
1072                                    }
1073                                } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
1074                                    let mut arc_ctor = arc_ctor.clone();
1075                                    if arc.start == segment_id {
1076                                        arc_ctor.start = ctor.position;
1077                                    } else if arc.end == segment_id {
1078                                        arc_ctor.end = ctor.position;
1079                                    } else {
1080                                        arc_ctor.center = ctor.position;
1081                                    }
1082                                    final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
1083                                } else {
1084                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1085                                        "Internal: Arc does not have arc ctor, but found {}",
1086                                        arc.ctor.human_friendly_kind_with_article()
1087                                    ))));
1088                                }
1089                                continue;
1090                            }
1091                            Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
1092                                if let Some(existing) = final_edits.get_mut(&owner_id) {
1093                                    let SegmentCtor::Circle(circle_ctor) = existing else {
1094                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1095                                            "Internal: Expected circle ctor for owner, but found {}",
1096                                            existing.human_friendly_kind_with_article()
1097                                        ))));
1098                                    };
1099                                    if circle.start == segment_id {
1100                                        circle_ctor.start = ctor.position;
1101                                    } else {
1102                                        circle_ctor.center = ctor.position;
1103                                    }
1104                                } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
1105                                    let mut circle_ctor = circle_ctor.clone();
1106                                    if circle.start == segment_id {
1107                                        circle_ctor.start = ctor.position;
1108                                    } else {
1109                                        circle_ctor.center = ctor.position;
1110                                    }
1111                                    final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
1112                                } else {
1113                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1114                                        "Internal: Circle does not have circle ctor, but found {}",
1115                                        circle.ctor.human_friendly_kind_with_article()
1116                                    ))));
1117                                }
1118                                continue;
1119                            }
1120                            Segment::ControlPointSpline(spline) if spline.controls.contains(&segment_id) => {
1121                                let Some(control_index) =
1122                                    spline.controls.iter().position(|control_id| *control_id == segment_id)
1123                                else {
1124                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1125                                        "Internal: Point is not part of owner's controlPointSpline segment: point={segment_id:?}, spline={owner_id:?}"
1126                                    ))));
1127                                };
1128                                if let Some(existing) = final_edits.get_mut(&owner_id) {
1129                                    let SegmentCtor::ControlPointSpline(spline_ctor) = existing else {
1130                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1131                                            "Internal: Expected controlPointSpline ctor for owner, but found {}",
1132                                            existing.human_friendly_kind_with_article()
1133                                        ))));
1134                                    };
1135                                    spline_ctor.points[control_index] = ctor.position;
1136                                } else if let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor {
1137                                    let mut spline_ctor = spline_ctor.clone();
1138                                    spline_ctor.points[control_index] = ctor.position;
1139                                    final_edits.insert(owner_id, SegmentCtor::ControlPointSpline(spline_ctor));
1140                                } else {
1141                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1142                                        "Internal: Control point spline does not have controlPointSpline ctor, but found {}",
1143                                        spline.ctor.human_friendly_kind_with_article()
1144                                    ))));
1145                                }
1146                                continue;
1147                            }
1148                            _ => {}
1149                        }
1150                    }
1151
1152                    // No owner, it's an individual point
1153                    final_edits.insert(segment_id, SegmentCtor::Point(ctor));
1154                }
1155                SegmentCtor::Line(ctor) => {
1156                    final_edits.insert(segment_id, SegmentCtor::Line(ctor));
1157                }
1158                SegmentCtor::Arc(ctor) => {
1159                    final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
1160                }
1161                SegmentCtor::Circle(ctor) => {
1162                    final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
1163                }
1164                SegmentCtor::ControlPointSpline(ctor) => {
1165                    final_edits.insert(segment_id, SegmentCtor::ControlPointSpline(ctor));
1166                }
1167            }
1168        }
1169
1170        for (segment_id, ctor) in final_edits {
1171            match ctor {
1172                SegmentCtor::Point(ctor) => self
1173                    .edit_point(&mut new_ast, sketch, segment_id, ctor)
1174                    .map_err(KclErrorWithOutputs::no_outputs)?,
1175                SegmentCtor::Line(ctor) => self
1176                    .edit_line(&mut new_ast, sketch, segment_id, ctor)
1177                    .map_err(KclErrorWithOutputs::no_outputs)?,
1178                SegmentCtor::Arc(ctor) => self
1179                    .edit_arc(&mut new_ast, sketch, segment_id, ctor)
1180                    .map_err(KclErrorWithOutputs::no_outputs)?,
1181                SegmentCtor::Circle(ctor) => self
1182                    .edit_circle(&mut new_ast, sketch, segment_id, ctor)
1183                    .map_err(KclErrorWithOutputs::no_outputs)?,
1184                SegmentCtor::ControlPointSpline(ctor) => self
1185                    .edit_control_point_spline(&mut new_ast, sketch, segment_id, ctor)
1186                    .map_err(KclErrorWithOutputs::no_outputs)?,
1187            }
1188        }
1189        for edit in constraint_label_edits {
1190            self.mutate_constraint_label_position(&mut new_ast, edit.constraint_id, edit.label_position)
1191                .map_err(KclErrorWithOutputs::no_outputs)?;
1192        }
1193        let (source_delta, mut scene_graph_delta) = self
1194            .execute_after_edit(
1195                ctx,
1196                sketch,
1197                sketch_block_ref,
1198                &mut new_ast,
1199                ExecuteAfterEditOptions {
1200                    segment_ids_edited: drag_anchor_segment_ids,
1201                    edit_kind: EditDeleteKind::Edit,
1202                    commit_solved_initial_guesses,
1203                },
1204            )
1205            .await?;
1206        if invalidates_ids {
1207            scene_graph_delta.invalidates_ids = true;
1208        }
1209        Ok((source_delta, scene_graph_delta))
1210    }
1211
1212    async fn delete_objects(
1213        &mut self,
1214        ctx: &ExecutorContext,
1215        _version: Version,
1216        sketch: ObjectId,
1217        constraint_ids: Vec<ObjectId>,
1218        segment_ids: Vec<ObjectId>,
1219    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1220        // TODO: Check version.
1221        let sketch_block_ref =
1222            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1223
1224        // Deduplicate IDs.
1225        let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1226        let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
1227
1228        // If a point is owned by a Line/Arc, we want to delete the owner, which will
1229        // also delete the point, as well as other points that are owned by the owner.
1230        let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
1231
1232        for segment_id in segment_ids_set.iter().copied() {
1233            let owner_id = self.scene_graph.objects.get(segment_id.0).and_then(|segment_object| {
1234                let ObjectKind::Segment { segment } = &segment_object.kind else {
1235                    return None;
1236                };
1237                match segment {
1238                    Segment::Point(point) => point.owner,
1239                    Segment::Line(line) => line.owner,
1240                    _ => None,
1241                }
1242            });
1243
1244            if let Some(owner_id) = owner_id
1245                && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
1246                && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
1247                && matches!(
1248                    owner_segment,
1249                    Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) | Segment::ControlPointSpline(_)
1250                )
1251            {
1252                // segment is owned -> delete the owner
1253                resolved_segment_ids_to_delete.insert(owner_id);
1254            } else {
1255                // segment is not owned by anything -> can be deleted
1256                resolved_segment_ids_to_delete.insert(segment_id);
1257            }
1258        }
1259        let referenced_constraint_ids = self
1260            .find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)
1261            .map_err(KclErrorWithOutputs::no_outputs)?;
1262
1263        let mut new_ast = self.program.ast.clone();
1264
1265        for constraint_id in referenced_constraint_ids {
1266            if constraint_ids_set.contains(&constraint_id) {
1267                continue;
1268            }
1269
1270            let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1271                KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Constraint not found: {constraint_id:?}")))
1272            })?;
1273            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
1274                return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1275                    "Object is not a constraint, it is {}",
1276                    constraint_object.kind.human_friendly_kind_with_article()
1277                ))));
1278            };
1279
1280            match constraint {
1281                Constraint::Coincident(coincident) => {
1282                    let remaining_segments =
1283                        self.remaining_constraint_segments(&coincident.segments, &resolved_segment_ids_to_delete);
1284
1285                    // If there are at least 2 segments left in the constraint: keep it, otherwise delete it.
1286                    if remaining_segments.len() >= 2 {
1287                        self.edit_coincident_constraint(&mut new_ast, constraint_id, remaining_segments)
1288                            .map_err(KclErrorWithOutputs::no_outputs)?;
1289                    } else {
1290                        constraint_ids_set.insert(constraint_id);
1291                    }
1292                }
1293                Constraint::EqualRadius(equal_radius) => {
1294                    let remaining_input = equal_radius
1295                        .input
1296                        .iter()
1297                        .copied()
1298                        .filter(|segment_id| {
1299                            !self.segment_will_be_deleted(*segment_id, &resolved_segment_ids_to_delete)
1300                        })
1301                        .collect::<Vec<_>>();
1302
1303                    if remaining_input.len() >= 2 {
1304                        self.edit_equal_radius_constraint(&mut new_ast, constraint_id, remaining_input)
1305                            .map_err(KclErrorWithOutputs::no_outputs)?;
1306                    } else {
1307                        constraint_ids_set.insert(constraint_id);
1308                    }
1309                }
1310                Constraint::LinesEqualLength(lines_equal_length) => {
1311                    let remaining_lines = lines_equal_length
1312                        .lines
1313                        .iter()
1314                        .copied()
1315                        .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1316                        .collect::<Vec<_>>();
1317
1318                    // Equal length constraint is only valid with at least 2 lines
1319                    if remaining_lines.len() >= 2 {
1320                        self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)
1321                            .map_err(KclErrorWithOutputs::no_outputs)?;
1322                    } else {
1323                        constraint_ids_set.insert(constraint_id);
1324                    }
1325                }
1326                Constraint::Parallel(parallel) => {
1327                    let remaining_lines = parallel
1328                        .lines
1329                        .iter()
1330                        .copied()
1331                        .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1332                        .collect::<Vec<_>>();
1333
1334                    if remaining_lines.len() >= 2 {
1335                        self.edit_parallel_constraint(&mut new_ast, constraint_id, remaining_lines)
1336                            .map_err(KclErrorWithOutputs::no_outputs)?;
1337                    } else {
1338                        constraint_ids_set.insert(constraint_id);
1339                    }
1340                }
1341                Constraint::Horizontal(Horizontal::Points { points }) => {
1342                    let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1343
1344                    if remaining_points.len() >= 2 {
1345                        self.edit_horizontal_points_constraint(&mut new_ast, constraint_id, remaining_points)
1346                            .map_err(KclErrorWithOutputs::no_outputs)?;
1347                    } else {
1348                        constraint_ids_set.insert(constraint_id);
1349                    }
1350                }
1351                Constraint::Vertical(Vertical::Points { points }) => {
1352                    let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1353
1354                    if remaining_points.len() >= 2 {
1355                        self.edit_vertical_points_constraint(&mut new_ast, constraint_id, remaining_points)
1356                            .map_err(KclErrorWithOutputs::no_outputs)?;
1357                    } else {
1358                        constraint_ids_set.insert(constraint_id);
1359                    }
1360                }
1361                Constraint::Fixed(fixed) => {
1362                    if fixed.points.iter().any(|fixed_point| {
1363                        self.segment_will_be_deleted(fixed_point.point, &resolved_segment_ids_to_delete)
1364                    }) {
1365                        constraint_ids_set.insert(constraint_id);
1366                    }
1367                }
1368                _ => {
1369                    // All other constraint types: if referenced by a segment -> delete the constraint
1370                    constraint_ids_set.insert(constraint_id);
1371                }
1372            }
1373        }
1374
1375        for constraint_id in constraint_ids_set {
1376            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1377                .map_err(KclErrorWithOutputs::no_outputs)?;
1378        }
1379        for segment_id in resolved_segment_ids_to_delete {
1380            self.delete_segment(&mut new_ast, sketch, segment_id)
1381                .map_err(KclErrorWithOutputs::no_outputs)?;
1382        }
1383
1384        self.execute_after_edit(
1385            ctx,
1386            sketch,
1387            sketch_block_ref,
1388            &mut new_ast,
1389            ExecuteAfterEditOptions {
1390                segment_ids_edited: Default::default(),
1391                edit_kind: EditDeleteKind::DeleteNonSketch,
1392                commit_solved_initial_guesses: true,
1393            },
1394        )
1395        .await
1396    }
1397
1398    async fn add_constraint(
1399        &mut self,
1400        ctx: &ExecutorContext,
1401        _version: Version,
1402        sketch: ObjectId,
1403        constraint: Constraint,
1404    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1405        // TODO: Check version.
1406
1407        // Save the original state as a backup - we'll restore it if anything fails
1408        let original_program = self.program.clone();
1409        let original_scene_graph = self.scene_graph.clone();
1410
1411        let mut new_ast = self.program.ast.clone();
1412        let sketch_block_ref = match constraint {
1413            Constraint::Coincident(coincident) => self
1414                .add_coincident(sketch, coincident, &mut new_ast)
1415                .await
1416                .map_err(KclErrorWithOutputs::no_outputs)?,
1417            Constraint::Distance(distance) => self
1418                .add_distance(sketch, distance, &mut new_ast)
1419                .await
1420                .map_err(KclErrorWithOutputs::no_outputs)?,
1421            Constraint::EqualRadius(equal_radius) => self
1422                .add_equal_radius(sketch, equal_radius, &mut new_ast)
1423                .await
1424                .map_err(KclErrorWithOutputs::no_outputs)?,
1425            Constraint::Fixed(fixed) => self
1426                .add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1427                .await
1428                .map_err(KclErrorWithOutputs::no_outputs)?,
1429            Constraint::HorizontalDistance(distance) => self
1430                .add_horizontal_distance(sketch, distance, &mut new_ast)
1431                .await
1432                .map_err(KclErrorWithOutputs::no_outputs)?,
1433            Constraint::VerticalDistance(distance) => self
1434                .add_vertical_distance(sketch, distance, &mut new_ast)
1435                .await
1436                .map_err(KclErrorWithOutputs::no_outputs)?,
1437            Constraint::Horizontal(horizontal) => self
1438                .add_horizontal(sketch, horizontal, &mut new_ast)
1439                .await
1440                .map_err(KclErrorWithOutputs::no_outputs)?,
1441            Constraint::LinesEqualLength(lines_equal_length) => self
1442                .add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1443                .await
1444                .map_err(KclErrorWithOutputs::no_outputs)?,
1445            Constraint::Midpoint(midpoint) => self
1446                .add_midpoint(sketch, midpoint, &mut new_ast)
1447                .await
1448                .map_err(KclErrorWithOutputs::no_outputs)?,
1449            Constraint::Parallel(parallel) => self
1450                .add_parallel(sketch, parallel, &mut new_ast)
1451                .await
1452                .map_err(KclErrorWithOutputs::no_outputs)?,
1453            Constraint::Perpendicular(perpendicular) => self
1454                .add_perpendicular(sketch, perpendicular, &mut new_ast)
1455                .await
1456                .map_err(KclErrorWithOutputs::no_outputs)?,
1457            Constraint::Radius(radius) => self
1458                .add_radius(sketch, radius, &mut new_ast)
1459                .await
1460                .map_err(KclErrorWithOutputs::no_outputs)?,
1461            Constraint::Diameter(diameter) => self
1462                .add_diameter(sketch, diameter, &mut new_ast)
1463                .await
1464                .map_err(KclErrorWithOutputs::no_outputs)?,
1465            Constraint::Symmetric(symmetric) => self
1466                .add_symmetric(sketch, symmetric, &mut new_ast)
1467                .await
1468                .map_err(KclErrorWithOutputs::no_outputs)?,
1469            Constraint::Vertical(vertical) => self
1470                .add_vertical(sketch, vertical, &mut new_ast)
1471                .await
1472                .map_err(KclErrorWithOutputs::no_outputs)?,
1473            Constraint::Angle(lines_at_angle) => self
1474                .add_angle(sketch, lines_at_angle, &mut new_ast)
1475                .await
1476                .map_err(KclErrorWithOutputs::no_outputs)?,
1477            Constraint::Tangent(tangent) => self
1478                .add_tangent(sketch, tangent, &mut new_ast)
1479                .await
1480                .map_err(KclErrorWithOutputs::no_outputs)?,
1481        };
1482
1483        let result = self
1484            .execute_after_add_constraint(ctx, sketch, sketch_block_ref, &mut new_ast)
1485            .await;
1486
1487        // If execution failed, restore the original state to prevent corruption
1488        if result.is_err() {
1489            self.program = original_program;
1490            self.scene_graph = original_scene_graph;
1491        }
1492
1493        result
1494    }
1495
1496    async fn chain_segment(
1497        &mut self,
1498        ctx: &ExecutorContext,
1499        version: Version,
1500        sketch: ObjectId,
1501        previous_segment_end_point_id: ObjectId,
1502        segment: SegmentCtor,
1503        _label: Option<String>,
1504    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1505        // TODO: Check version.
1506
1507        // First, add the segment (line) to get its start point ID
1508        let SegmentCtor::Line(line_ctor) = segment else {
1509            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1510                "chain_segment currently only supports Line segments, got {}",
1511                segment.human_friendly_kind_with_article(),
1512            ))));
1513        };
1514
1515        // Add the line segment first - this updates self.program and self.scene_graph
1516        let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
1517
1518        // Find the new line's start point ID from the updated scene graph
1519        // add_line updates self.scene_graph, so we can use that
1520        let new_line_id = first_scene_delta
1521            .new_objects
1522            .iter()
1523            .find(|&obj_id| {
1524                let obj = self.scene_graph.objects.get(obj_id.0);
1525                if let Some(obj) = obj {
1526                    matches!(
1527                        &obj.kind,
1528                        ObjectKind::Segment {
1529                            segment: Segment::Line(_)
1530                        }
1531                    )
1532                } else {
1533                    false
1534                }
1535            })
1536            .ok_or_else(|| {
1537                KclErrorWithOutputs::no_outputs(KclError::refactor(
1538                    "Failed to find new line segment in scene graph".to_string(),
1539                ))
1540            })?;
1541
1542        let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| {
1543            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1544                "New line object not found: {new_line_id:?}"
1545            )))
1546        })?;
1547
1548        let ObjectKind::Segment {
1549            segment: new_line_segment,
1550        } = &new_line_obj.kind
1551        else {
1552            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1553                "Object is not a segment: {new_line_obj:?}"
1554            ))));
1555        };
1556
1557        let Segment::Line(new_line) = new_line_segment else {
1558            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1559                "Segment is not a line: {new_line_segment:?}"
1560            ))));
1561        };
1562
1563        let new_line_start_point_id = new_line.start;
1564
1565        // Now add the coincident constraint between the previous end point and the new line's start point.
1566        let coincident = Coincident {
1567            segments: vec![previous_segment_end_point_id.into(), new_line_start_point_id.into()],
1568        };
1569
1570        let (final_src_delta, final_scene_delta) = self
1571            .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
1572            .await?;
1573
1574        // Combine new objects from the line addition and the constraint addition.
1575        // Both add_line and add_constraint now populate new_objects correctly.
1576        let mut combined_new_objects = first_scene_delta.new_objects.clone();
1577        combined_new_objects.extend(final_scene_delta.new_objects);
1578
1579        let scene_graph_delta = SceneGraphDelta {
1580            new_graph: self.scene_graph_for_ui(),
1581            invalidates_ids: false,
1582            new_objects: combined_new_objects,
1583            exec_outcome: final_scene_delta.exec_outcome,
1584        };
1585
1586        Ok((final_src_delta, scene_graph_delta))
1587    }
1588
1589    // Edit only the value, e.g. `distance(...) == 5mm` to `distance(...) == 7mm`.
1590    async fn edit_constraint_value(
1591        &mut self,
1592        ctx: &ExecutorContext,
1593        _version: Version,
1594        sketch: ObjectId,
1595        constraint_id: ObjectId,
1596        value_expression: String,
1597    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1598        // TODO: Check version.
1599        let sketch_block_ref =
1600            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1601
1602        let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1603            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1604        })?;
1605        if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
1606            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1607                "Object is not a constraint: {constraint_id:?}"
1608            ))));
1609        }
1610
1611        let mut new_ast = self.program.ast.clone();
1612
1613        // Parse the expression string into an AST node.
1614        let (parsed, errors) = Program::parse(&value_expression).map_err(|e| {
1615            KclErrorWithOutputs::no_outputs(KclError::refactor(format_kcl_error_message(
1616                "Invalid constraint value",
1617                &e,
1618            )))
1619        })?;
1620        if !errors.is_empty() {
1621            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1622                format_compilation_issues("Invalid constraint value", &errors),
1623            )));
1624        }
1625        let mut parsed = parsed.ok_or_else(|| {
1626            KclErrorWithOutputs::no_outputs(KclError::refactor("No AST produced from value expression".to_string()))
1627        })?;
1628        if parsed.ast.body.is_empty() {
1629            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1630                "Empty value expression".to_string(),
1631            )));
1632        }
1633        let first = parsed.ast.body.remove(0);
1634        let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
1635            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1636                "Value expression must be a simple expression".to_string(),
1637            )));
1638        };
1639
1640        let new_value: ast::BinaryPart = expr_stmt
1641            .inner
1642            .expression
1643            .try_into()
1644            .map_err(|e: String| KclErrorWithOutputs::no_outputs(KclError::refactor(e)))?;
1645
1646        self.mutate_ast(
1647            &mut new_ast,
1648            constraint_id,
1649            AstMutateCommand::EditConstraintValue { value: new_value },
1650        )
1651        .map_err(KclErrorWithOutputs::no_outputs)?;
1652
1653        self.execute_after_edit(
1654            ctx,
1655            sketch,
1656            sketch_block_ref,
1657            &mut new_ast,
1658            ExecuteAfterEditOptions {
1659                segment_ids_edited: Default::default(),
1660                edit_kind: EditDeleteKind::Edit,
1661                commit_solved_initial_guesses: true,
1662            },
1663        )
1664        .await
1665    }
1666
1667    async fn edit_distance_constraint_label_position(
1668        &mut self,
1669        ctx: &ExecutorContext,
1670        _version: Version,
1671        sketch: ObjectId,
1672        constraint_id: ObjectId,
1673        label_position: Point2d<Number>,
1674        anchor_segment_ids: Vec<ObjectId>,
1675    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1676        // TODO: Check version.
1677        let sketch_block_ref =
1678            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1679
1680        let mut new_ast = self.program.ast.clone();
1681        self.mutate_constraint_label_position(&mut new_ast, constraint_id, label_position)
1682            .map_err(KclErrorWithOutputs::no_outputs)?;
1683        let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
1684
1685        self.execute_after_edit(
1686            ctx,
1687            sketch,
1688            sketch_block_ref,
1689            &mut new_ast,
1690            ExecuteAfterEditOptions {
1691                segment_ids_edited: anchor_segment_ids.into_iter().collect(),
1692                edit_kind: EditDeleteKind::Edit,
1693                commit_solved_initial_guesses,
1694            },
1695        )
1696        .await
1697    }
1698
1699    /// Splitting a segment means creating a new segment, editing the old one, and then
1700    /// migrating a bunch of the constraints from the original segment to the new one
1701    /// (i.e. deleting them and re-adding them on the other segment).
1702    ///
1703    /// To keep this efficient we require as few executions as possible: we create the
1704    /// new segment first (to get its id), then do all edits and new constraints, and
1705    /// do all deletes at the end (since deletes invalidate ids).
1706    async fn batch_split_segment_operations(
1707        &mut self,
1708        ctx: &ExecutorContext,
1709        _version: Version,
1710        sketch: ObjectId,
1711        edit_segments: Vec<ExistingSegmentCtor>,
1712        add_constraints: Vec<Constraint>,
1713        delete_constraint_ids: Vec<ObjectId>,
1714        _new_segment_info: sketch::NewSegmentInfo,
1715    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1716        // TODO: Check version.
1717        let sketch_block_ref =
1718            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1719
1720        let mut new_ast = self.program.ast.clone();
1721        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1722
1723        // Step 1: Edit segments
1724        for segment in edit_segments {
1725            segment_ids_edited.insert(segment.id);
1726            match segment.ctor {
1727                SegmentCtor::Point(ctor) => self
1728                    .edit_point(&mut new_ast, sketch, segment.id, ctor)
1729                    .map_err(KclErrorWithOutputs::no_outputs)?,
1730                SegmentCtor::Line(ctor) => self
1731                    .edit_line(&mut new_ast, sketch, segment.id, ctor)
1732                    .map_err(KclErrorWithOutputs::no_outputs)?,
1733                SegmentCtor::Arc(ctor) => self
1734                    .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1735                    .map_err(KclErrorWithOutputs::no_outputs)?,
1736                SegmentCtor::Circle(ctor) => self
1737                    .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1738                    .map_err(KclErrorWithOutputs::no_outputs)?,
1739                SegmentCtor::ControlPointSpline(ctor) => self
1740                    .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1741                    .map_err(KclErrorWithOutputs::no_outputs)?,
1742            }
1743        }
1744
1745        // Step 2: Add all constraints
1746        for constraint in add_constraints {
1747            match constraint {
1748                Constraint::Coincident(coincident) => {
1749                    self.add_coincident(sketch, coincident, &mut new_ast)
1750                        .await
1751                        .map_err(KclErrorWithOutputs::no_outputs)?;
1752                }
1753                Constraint::Distance(distance) => {
1754                    self.add_distance(sketch, distance, &mut new_ast)
1755                        .await
1756                        .map_err(KclErrorWithOutputs::no_outputs)?;
1757                }
1758                Constraint::EqualRadius(equal_radius) => {
1759                    self.add_equal_radius(sketch, equal_radius, &mut new_ast)
1760                        .await
1761                        .map_err(KclErrorWithOutputs::no_outputs)?;
1762                }
1763                Constraint::Fixed(fixed) => {
1764                    self.add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1765                        .await
1766                        .map_err(KclErrorWithOutputs::no_outputs)?;
1767                }
1768                Constraint::HorizontalDistance(distance) => {
1769                    self.add_horizontal_distance(sketch, distance, &mut new_ast)
1770                        .await
1771                        .map_err(KclErrorWithOutputs::no_outputs)?;
1772                }
1773                Constraint::VerticalDistance(distance) => {
1774                    self.add_vertical_distance(sketch, distance, &mut new_ast)
1775                        .await
1776                        .map_err(KclErrorWithOutputs::no_outputs)?;
1777                }
1778                Constraint::Horizontal(horizontal) => {
1779                    self.add_horizontal(sketch, horizontal, &mut new_ast)
1780                        .await
1781                        .map_err(KclErrorWithOutputs::no_outputs)?;
1782                }
1783                Constraint::LinesEqualLength(lines_equal_length) => {
1784                    self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1785                        .await
1786                        .map_err(KclErrorWithOutputs::no_outputs)?;
1787                }
1788                Constraint::Midpoint(midpoint) => {
1789                    self.add_midpoint(sketch, midpoint, &mut new_ast)
1790                        .await
1791                        .map_err(KclErrorWithOutputs::no_outputs)?;
1792                }
1793                Constraint::Parallel(parallel) => {
1794                    self.add_parallel(sketch, parallel, &mut new_ast)
1795                        .await
1796                        .map_err(KclErrorWithOutputs::no_outputs)?;
1797                }
1798                Constraint::Perpendicular(perpendicular) => {
1799                    self.add_perpendicular(sketch, perpendicular, &mut new_ast)
1800                        .await
1801                        .map_err(KclErrorWithOutputs::no_outputs)?;
1802                }
1803                Constraint::Vertical(vertical) => {
1804                    self.add_vertical(sketch, vertical, &mut new_ast)
1805                        .await
1806                        .map_err(KclErrorWithOutputs::no_outputs)?;
1807                }
1808                Constraint::Diameter(diameter) => {
1809                    self.add_diameter(sketch, diameter, &mut new_ast)
1810                        .await
1811                        .map_err(KclErrorWithOutputs::no_outputs)?;
1812                }
1813                Constraint::Radius(radius) => {
1814                    self.add_radius(sketch, radius, &mut new_ast)
1815                        .await
1816                        .map_err(KclErrorWithOutputs::no_outputs)?;
1817                }
1818                Constraint::Symmetric(symmetric) => {
1819                    self.add_symmetric(sketch, symmetric, &mut new_ast)
1820                        .await
1821                        .map_err(KclErrorWithOutputs::no_outputs)?;
1822                }
1823                Constraint::Angle(angle) => {
1824                    self.add_angle(sketch, angle, &mut new_ast)
1825                        .await
1826                        .map_err(KclErrorWithOutputs::no_outputs)?;
1827                }
1828                Constraint::Tangent(tangent) => {
1829                    self.add_tangent(sketch, tangent, &mut new_ast)
1830                        .await
1831                        .map_err(KclErrorWithOutputs::no_outputs)?;
1832                }
1833            }
1834        }
1835
1836        // Step 3: Delete constraints (must be last since deletes can invalidate IDs)
1837        let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1838
1839        let has_constraint_deletions = !constraint_ids_set.is_empty();
1840        for constraint_id in constraint_ids_set {
1841            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1842                .map_err(KclErrorWithOutputs::no_outputs)?;
1843        }
1844
1845        // Step 4: Execute once at the end
1846        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
1847        // But we'll manually set invalidates_ids: true if we deleted constraints
1848        let (source_delta, mut scene_graph_delta) = self
1849            .execute_after_edit(
1850                ctx,
1851                sketch,
1852                sketch_block_ref,
1853                &mut new_ast,
1854                ExecuteAfterEditOptions {
1855                    segment_ids_edited,
1856                    edit_kind: EditDeleteKind::Edit,
1857                    commit_solved_initial_guesses: true,
1858                },
1859            )
1860            .await?;
1861
1862        // If we deleted constraints, set invalidates_ids: true
1863        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
1864        if has_constraint_deletions {
1865            scene_graph_delta.invalidates_ids = true;
1866        }
1867
1868        Ok((source_delta, scene_graph_delta))
1869    }
1870
1871    async fn batch_tail_cut_operations(
1872        &mut self,
1873        ctx: &ExecutorContext,
1874        _version: Version,
1875        sketch: ObjectId,
1876        edit_segments: Vec<ExistingSegmentCtor>,
1877        add_constraints: Vec<Constraint>,
1878        delete_constraint_ids: Vec<ObjectId>,
1879        additional_edited_segment_ids: Vec<ObjectId>,
1880    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1881        let sketch_block_ref =
1882            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1883
1884        let mut new_ast = self.program.ast.clone();
1885        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1886
1887        // Step 1: Edit segments (usually a single segment for tail cut)
1888        for segment in edit_segments {
1889            segment_ids_edited.insert(segment.id);
1890            match segment.ctor {
1891                SegmentCtor::Point(ctor) => self
1892                    .edit_point(&mut new_ast, sketch, segment.id, ctor)
1893                    .map_err(KclErrorWithOutputs::no_outputs)?,
1894                SegmentCtor::Line(ctor) => self
1895                    .edit_line(&mut new_ast, sketch, segment.id, ctor)
1896                    .map_err(KclErrorWithOutputs::no_outputs)?,
1897                SegmentCtor::Arc(ctor) => self
1898                    .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1899                    .map_err(KclErrorWithOutputs::no_outputs)?,
1900                SegmentCtor::Circle(ctor) => self
1901                    .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1902                    .map_err(KclErrorWithOutputs::no_outputs)?,
1903                SegmentCtor::ControlPointSpline(ctor) => self
1904                    .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1905                    .map_err(KclErrorWithOutputs::no_outputs)?,
1906            }
1907        }
1908
1909        segment_ids_edited.extend(additional_edited_segment_ids);
1910
1911        // Step 2: Add coincident constraints
1912        for constraint in add_constraints {
1913            match constraint {
1914                Constraint::Coincident(coincident) => {
1915                    self.add_coincident(sketch, coincident, &mut new_ast)
1916                        .await
1917                        .map_err(KclErrorWithOutputs::no_outputs)?;
1918                }
1919                other => {
1920                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1921                        "unsupported constraint in tail cut batch: {other:?}"
1922                    ))));
1923                }
1924            }
1925        }
1926
1927        // Step 3: Delete constraints (if any)
1928        let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1929
1930        let has_constraint_deletions = !constraint_ids_set.is_empty();
1931        for constraint_id in constraint_ids_set {
1932            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1933                .map_err(KclErrorWithOutputs::no_outputs)?;
1934        }
1935
1936        // Step 4: Single execute_after_edit
1937        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
1938        // But we'll manually set invalidates_ids: true if we deleted constraints
1939        let (source_delta, mut scene_graph_delta) = self
1940            .execute_after_edit(
1941                ctx,
1942                sketch,
1943                sketch_block_ref,
1944                &mut new_ast,
1945                ExecuteAfterEditOptions {
1946                    segment_ids_edited,
1947                    edit_kind: EditDeleteKind::Edit,
1948                    commit_solved_initial_guesses: true,
1949                },
1950            )
1951            .await?;
1952
1953        // If we deleted constraints, set invalidates_ids: true
1954        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
1955        if has_constraint_deletions {
1956            scene_graph_delta.invalidates_ids = true;
1957        }
1958
1959        Ok((source_delta, scene_graph_delta))
1960    }
1961}
1962
1963impl FrontendState {
1964    pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1965        self.program = program.clone();
1966
1967        // Execute so that the objects are updated and available for the next
1968        // API call.
1969        // This always uses engine execution (not mock) so that things are cached.
1970        // Engine execution now runs freedom analysis automatically.
1971        // Keep existing checkpoints alive here. History may still reference
1972        // older committed sketch states across a direct-edit boundary, and a
1973        // checkpoint restore is a full state replacement anyway. We append a
1974        // fresh baseline checkpoint after the full execution below.
1975        // Clear the freedom cache since IDs might have changed after direct editing
1976        // and we're about to run freedom analysis which will repopulate it.
1977        self.point_freedom_cache.clear();
1978        match ctx.run_with_caching(program).await {
1979            Ok(outcome) => {
1980                let outcome = self.update_state_after_exec(outcome, true);
1981                let checkpoint_id = self
1982                    .create_sketch_checkpoint(outcome.clone())
1983                    .await
1984                    .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
1985                Ok(SetProgramOutcome::Success {
1986                    scene_graph: Box::new(self.scene_graph_for_ui()),
1987                    exec_outcome: Box::new(outcome),
1988                    checkpoint_id: Some(checkpoint_id),
1989                })
1990            }
1991            Err(mut err) => {
1992                // Don't return an error just because execution failed. Instead,
1993                // update state as much as possible.
1994                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1995                self.update_state_after_exec(outcome, true);
1996                err.scene_graph = Some(self.scene_graph_for_ui());
1997                Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
1998            }
1999        }
2000    }
2001
2002    /// Decorate engine execution such that our state is updated and the scene
2003    /// graph is added to the return.
2004    pub async fn engine_execute(
2005        &mut self,
2006        ctx: &ExecutorContext,
2007        program: Program,
2008    ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
2009        self.program = program.clone();
2010
2011        // Engine execution now runs freedom analysis automatically. Clear the
2012        // freedom cache since IDs might have changed after direct editing, and
2013        // we're about to run freedom analysis which will repopulate it.
2014        self.point_freedom_cache.clear();
2015        match ctx.run_with_caching(program).await {
2016            Ok(outcome) => {
2017                let outcome = self.update_state_after_exec(outcome, true);
2018                Ok(SceneGraphDelta {
2019                    new_graph: self.scene_graph_for_ui(),
2020                    exec_outcome: outcome,
2021                    // We don't know what the new objects are.
2022                    new_objects: Default::default(),
2023                    // We don't know if IDs were invalidated.
2024                    invalidates_ids: Default::default(),
2025                })
2026            }
2027            Err(mut err) => {
2028                // Update state as much as possible, even when there's an error.
2029                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
2030                self.update_state_after_exec(outcome, true);
2031                err.scene_graph = Some(self.scene_graph_for_ui());
2032                Err(err)
2033            }
2034        }
2035    }
2036
2037    fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
2038        if matches!(err.error, KclError::EngineHangup { .. }) {
2039            // It's not ideal to special-case this, but this error is very
2040            // common during development, and it causes confusing downstream
2041            // errors that have nothing to do with the actual problem.
2042            return Err(err);
2043        }
2044
2045        let KclErrorWithOutputs {
2046            error,
2047            mut non_fatal,
2048            variables,
2049            operations,
2050            artifact_graph,
2051            scene_objects,
2052            source_range_to_object,
2053            var_solutions,
2054            refactor_metadata,
2055            filenames,
2056            source_files,
2057            default_planes,
2058            ..
2059        } = err;
2060
2061        non_fatal.push(CompilationIssue::fatal(issue_source_range(&error), error.get_message()));
2062
2063        Ok(ExecOutcome {
2064            variables,
2065            filenames,
2066            operations,
2067            artifact_graph,
2068            scene_objects,
2069            source_range_to_object,
2070            var_solutions,
2071            refactor_metadata,
2072            issues: non_fatal,
2073            source_files,
2074            default_planes,
2075        })
2076    }
2077
2078    async fn add_point(
2079        &mut self,
2080        ctx: &ExecutorContext,
2081        sketch: ObjectId,
2082        ctor: PointCtor,
2083    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2084        // Create updated KCL source from args.
2085        let at_ast = to_ast_point2d(&ctor.position)
2086            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2087        let point_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2088            callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
2089            unlabeled: None,
2090            arguments: vec![ast::LabeledArg {
2091                label: Some(ast::Identifier::new(POINT_AT_PARAM)),
2092                arg: at_ast,
2093            }],
2094            digest: None,
2095            non_code_meta: Default::default(),
2096        })));
2097
2098        // Look up existing sketch.
2099        let sketch_id = sketch;
2100        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2101            #[cfg(target_arch = "wasm32")]
2102            web_sys::console::error_1(
2103                &format!(
2104                    "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
2105                    self.scene_graph.objects
2106                )
2107                .into(),
2108            );
2109            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2110        })?;
2111        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2112            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2113                "Object is not a sketch, it is {}",
2114                sketch_object.kind.human_friendly_kind_with_article(),
2115            ))));
2116        };
2117        // Add the point to the AST of the sketch block.
2118        let mut new_ast = self.program.ast.clone();
2119        let (sketch_block_ref, _) = self
2120            .mutate_ast(
2121                &mut new_ast,
2122                sketch_id,
2123                AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
2124            )
2125            .map_err(KclErrorWithOutputs::no_outputs)?;
2126        // Convert to string source to create real source ranges.
2127        let new_source = source_from_ast(&new_ast);
2128        // Parse the new KCL source.
2129        let new_program = parse_frontend_mutation_source(
2130            &new_source,
2131            "Error parsing KCL source after adding point",
2132            "No AST produced after adding point",
2133        )?;
2134
2135        let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2136            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2137                "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
2138            )))
2139        })?;
2140
2141        // Make sure to only set this if there are no errors.
2142        self.program = new_program.clone();
2143
2144        // Truncate after the sketch block for mock execution.
2145        let mut truncated_program = new_program;
2146        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2147            .map_err(KclErrorWithOutputs::no_outputs)?;
2148
2149        // Execute.
2150        let outcome = ctx
2151            .run_mock(
2152                &truncated_program,
2153                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2154            )
2155            .await?;
2156
2157        let new_object_ids = {
2158            let make_err =
2159                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2160            let segment_id = outcome
2161                .source_range_to_object
2162                .get(&point_node_ref.range)
2163                .copied()
2164                .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
2165            let segment_object = outcome
2166                .scene_objects
2167                .get(segment_id.0)
2168                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2169            let ObjectKind::Segment { segment } = &segment_object.kind else {
2170                return Err(make_err(format!(
2171                    "Object is not a segment, it is {}",
2172                    segment_object.kind.human_friendly_kind_with_article()
2173                )));
2174            };
2175            let Segment::Point(_) = segment else {
2176                return Err(make_err(format!(
2177                    "Segment is not a point, it is {}",
2178                    segment.human_friendly_kind_with_article()
2179                )));
2180            };
2181            vec![segment_id]
2182        };
2183        let src_delta = SourceDelta { text: new_source };
2184        // Uses .no_freedom_analysis() so freedom_analysis: false
2185        let outcome = self.update_state_after_exec(outcome, false);
2186        let scene_graph_delta = SceneGraphDelta {
2187            new_graph: self.scene_graph_for_ui(),
2188            invalidates_ids: false,
2189            new_objects: new_object_ids,
2190            exec_outcome: outcome,
2191        };
2192        Ok((src_delta, scene_graph_delta))
2193    }
2194
2195    async fn add_line(
2196        &mut self,
2197        ctx: &ExecutorContext,
2198        sketch: ObjectId,
2199        ctor: LineCtor,
2200    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2201        // Create updated KCL source from args.
2202        let start_ast = to_ast_point2d(&ctor.start)
2203            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2204        let end_ast = to_ast_point2d(&ctor.end)
2205            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2206        let mut arguments = vec![
2207            ast::LabeledArg {
2208                label: Some(ast::Identifier::new(LINE_START_PARAM)),
2209                arg: start_ast,
2210            },
2211            ast::LabeledArg {
2212                label: Some(ast::Identifier::new(LINE_END_PARAM)),
2213                arg: end_ast,
2214            },
2215        ];
2216        // Add construction kwarg if construction is Some(true)
2217        if ctor.construction == Some(true) {
2218            arguments.push(ast::LabeledArg {
2219                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2220                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2221                    value: ast::LiteralValue::Bool(true),
2222                    raw: "true".to_string(),
2223                    digest: None,
2224                }))),
2225            });
2226        }
2227        let line_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2228            callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
2229            unlabeled: None,
2230            arguments,
2231            digest: None,
2232            non_code_meta: Default::default(),
2233        })));
2234
2235        // Look up existing sketch.
2236        let sketch_id = sketch;
2237        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2238            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2239        })?;
2240        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2241            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2242                "Object is not a sketch, it is {}",
2243                sketch_object.kind.human_friendly_kind_with_article(),
2244            ))));
2245        };
2246        // Add the line to the AST of the sketch block.
2247        let mut new_ast = self.program.ast.clone();
2248        let (sketch_block_ref, _) = self
2249            .mutate_ast(
2250                &mut new_ast,
2251                sketch_id,
2252                AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
2253            )
2254            .map_err(KclErrorWithOutputs::no_outputs)?;
2255        // Convert to string source to create real source ranges.
2256        let new_source = source_from_ast(&new_ast);
2257        // Parse the new KCL source.
2258        let new_program = parse_frontend_mutation_source(
2259            &new_source,
2260            "Error parsing KCL source after adding line",
2261            "No AST produced after adding line",
2262        )?;
2263
2264        let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2265            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2266                "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
2267            )))
2268        })?;
2269
2270        // Make sure to only set this if there are no errors.
2271        self.program = new_program.clone();
2272
2273        // Truncate after the sketch block for mock execution.
2274        let mut truncated_program = new_program;
2275        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2276            .map_err(KclErrorWithOutputs::no_outputs)?;
2277
2278        // Execute.
2279        let outcome = ctx
2280            .run_mock(
2281                &truncated_program,
2282                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2283            )
2284            .await?;
2285
2286        let new_object_ids = {
2287            let make_err =
2288                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2289            let segment_id = outcome
2290                .source_range_to_object
2291                .get(&line_node_ref.range)
2292                .copied()
2293                .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
2294            let segment_object = outcome
2295                .scene_object_by_id(segment_id)
2296                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2297            let ObjectKind::Segment { segment } = &segment_object.kind else {
2298                return Err(make_err(format!(
2299                    "Object is not a segment, it is {}",
2300                    segment_object.kind.human_friendly_kind_with_article()
2301                )));
2302            };
2303            let Segment::Line(line) = segment else {
2304                return Err(make_err(format!(
2305                    "Segment is not a line, it is {}",
2306                    segment.human_friendly_kind_with_article()
2307                )));
2308            };
2309            vec![line.start, line.end, segment_id]
2310        };
2311        let src_delta = SourceDelta { text: new_source };
2312        // Uses .no_freedom_analysis() so freedom_analysis: false
2313        let outcome = self.update_state_after_exec(outcome, false);
2314        let scene_graph_delta = SceneGraphDelta {
2315            new_graph: self.scene_graph_for_ui(),
2316            invalidates_ids: false,
2317            new_objects: new_object_ids,
2318            exec_outcome: outcome,
2319        };
2320        Ok((src_delta, scene_graph_delta))
2321    }
2322
2323    async fn add_arc(
2324        &mut self,
2325        ctx: &ExecutorContext,
2326        sketch: ObjectId,
2327        ctor: ArcCtor,
2328    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2329        // Create updated KCL source from args.
2330        let start_ast = to_ast_point2d(&ctor.start)
2331            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2332        let end_ast = to_ast_point2d(&ctor.end)
2333            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2334        let center_ast = to_ast_point2d(&ctor.center)
2335            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2336        let mut arguments = vec![
2337            ast::LabeledArg {
2338                label: Some(ast::Identifier::new(ARC_START_PARAM)),
2339                arg: start_ast,
2340            },
2341            ast::LabeledArg {
2342                label: Some(ast::Identifier::new(ARC_END_PARAM)),
2343                arg: end_ast,
2344            },
2345            ast::LabeledArg {
2346                label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
2347                arg: center_ast,
2348            },
2349        ];
2350        // Add direction kwarg if it's clockwise, since counterclockwise is the
2351        // default.
2352        if ctor.direction == Some(ArcDirection::Cw) {
2353            arguments.push(ast::LabeledArg {
2354                label: Some(ast::Identifier::new(ARC_DIRECTION_PARAM)),
2355                arg: ast::Expr::Name(BoxNode::new(ast::Name::new(ARC_DIRECTION_CW_NAME))),
2356            });
2357        }
2358        // Add construction kwarg if construction is Some(true)
2359        if ctor.construction == Some(true) {
2360            arguments.push(ast::LabeledArg {
2361                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2362                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2363                    value: ast::LiteralValue::Bool(true),
2364                    raw: "true".to_string(),
2365                    digest: None,
2366                }))),
2367            });
2368        }
2369        let arc_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2370            callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
2371            unlabeled: None,
2372            arguments,
2373            digest: None,
2374            non_code_meta: Default::default(),
2375        })));
2376
2377        // Look up existing sketch.
2378        let sketch_id = sketch;
2379        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2380            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2381        })?;
2382        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2383            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2384                "Object is not a sketch, it is {}",
2385                sketch_object.kind.human_friendly_kind_with_article(),
2386            ))));
2387        };
2388        // Add the arc to the AST of the sketch block.
2389        let mut new_ast = self.program.ast.clone();
2390        let (sketch_block_ref, _) = self
2391            .mutate_ast(
2392                &mut new_ast,
2393                sketch_id,
2394                AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
2395            )
2396            .map_err(KclErrorWithOutputs::no_outputs)?;
2397        // Convert to string source to create real source ranges.
2398        let new_source = source_from_ast(&new_ast);
2399        // Parse the new KCL source.
2400        let new_program = parse_frontend_mutation_source(
2401            &new_source,
2402            "Error parsing KCL source after adding arc",
2403            "No AST produced after adding arc",
2404        )?;
2405
2406        let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2407            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2408                "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
2409            )))
2410        })?;
2411
2412        // Make sure to only set this if there are no errors.
2413        self.program = new_program.clone();
2414
2415        // Truncate after the sketch block for mock execution.
2416        let mut truncated_program = new_program;
2417        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2418            .map_err(KclErrorWithOutputs::no_outputs)?;
2419
2420        // Execute.
2421        let outcome = ctx
2422            .run_mock(
2423                &truncated_program,
2424                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2425            )
2426            .await?;
2427
2428        let new_object_ids = {
2429            let make_err =
2430                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2431            let segment_id = outcome
2432                .source_range_to_object
2433                .get(&arc_node_ref.range)
2434                .copied()
2435                .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
2436            let segment_object = outcome
2437                .scene_objects
2438                .get(segment_id.0)
2439                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2440            let ObjectKind::Segment { segment } = &segment_object.kind else {
2441                return Err(make_err(format!(
2442                    "Object is not a segment, it is {}",
2443                    segment_object.kind.human_friendly_kind_with_article()
2444                )));
2445            };
2446            let Segment::Arc(arc) = segment else {
2447                return Err(make_err(format!(
2448                    "Segment is not an arc, it is {}",
2449                    segment.human_friendly_kind_with_article()
2450                )));
2451            };
2452            vec![arc.start, arc.end, arc.center, segment_id]
2453        };
2454        let src_delta = SourceDelta { text: new_source };
2455        // Uses .no_freedom_analysis() so freedom_analysis: false
2456        let outcome = self.update_state_after_exec(outcome, false);
2457        let scene_graph_delta = SceneGraphDelta {
2458            new_graph: self.scene_graph_for_ui(),
2459            invalidates_ids: false,
2460            new_objects: new_object_ids,
2461            exec_outcome: outcome,
2462        };
2463        Ok((src_delta, scene_graph_delta))
2464    }
2465
2466    async fn add_circle(
2467        &mut self,
2468        ctx: &ExecutorContext,
2469        sketch: ObjectId,
2470        ctor: CircleCtor,
2471    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2472        // Create updated KCL source from args.
2473        let start_ast = to_ast_point2d(&ctor.start)
2474            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2475        let center_ast = to_ast_point2d(&ctor.center)
2476            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2477        let mut arguments = vec![
2478            ast::LabeledArg {
2479                label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2480                arg: start_ast,
2481            },
2482            ast::LabeledArg {
2483                label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2484                arg: center_ast,
2485            },
2486        ];
2487        // Add construction kwarg if construction is Some(true)
2488        if ctor.construction == Some(true) {
2489            arguments.push(ast::LabeledArg {
2490                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2491                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2492                    value: ast::LiteralValue::Bool(true),
2493                    raw: "true".to_string(),
2494                    digest: None,
2495                }))),
2496            });
2497        }
2498        let circle_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2499            callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2500            unlabeled: None,
2501            arguments,
2502            digest: None,
2503            non_code_meta: Default::default(),
2504        })));
2505
2506        // Look up existing sketch.
2507        let sketch_id = sketch;
2508        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2509            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2510        })?;
2511        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2512            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2513                "Object is not a sketch, it is {}",
2514                sketch_object.kind.human_friendly_kind_with_article(),
2515            ))));
2516        };
2517        // Add the circle to the AST of the sketch block.
2518        let mut new_ast = self.program.ast.clone();
2519        let (sketch_block_ref, _) = self
2520            .mutate_ast(
2521                &mut new_ast,
2522                sketch_id,
2523                AstMutateCommand::AddSketchBlockVarDecl {
2524                    prefix: CIRCLE_VARIABLE.to_owned(),
2525                    expr: circle_ast,
2526                },
2527            )
2528            .map_err(KclErrorWithOutputs::no_outputs)?;
2529        // Convert to string source to create real source ranges.
2530        let new_source = source_from_ast(&new_ast);
2531        // Parse the new KCL source.
2532        let new_program = parse_frontend_mutation_source(
2533            &new_source,
2534            "Error parsing KCL source after adding circle",
2535            "No AST produced after adding circle",
2536        )?;
2537
2538        let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2539            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2540                "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2541            )))
2542        })?;
2543
2544        // Make sure to only set this if there are no errors.
2545        self.program = new_program.clone();
2546
2547        // Truncate after the sketch block for mock execution.
2548        let mut truncated_program = new_program;
2549        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2550            .map_err(KclErrorWithOutputs::no_outputs)?;
2551
2552        // Execute.
2553        let outcome = ctx
2554            .run_mock(
2555                &truncated_program,
2556                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2557            )
2558            .await?;
2559
2560        let new_object_ids = {
2561            let make_err =
2562                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2563            let segment_id = outcome
2564                .source_range_to_object
2565                .get(&circle_node_ref.range)
2566                .copied()
2567                .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2568            let segment_object = outcome
2569                .scene_objects
2570                .get(segment_id.0)
2571                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2572            let ObjectKind::Segment { segment } = &segment_object.kind else {
2573                return Err(make_err(format!(
2574                    "Object is not a segment, it is {}",
2575                    segment_object.kind.human_friendly_kind_with_article()
2576                )));
2577            };
2578            let Segment::Circle(circle) = segment else {
2579                return Err(make_err(format!(
2580                    "Segment is not a circle, it is {}",
2581                    segment.human_friendly_kind_with_article()
2582                )));
2583            };
2584            vec![circle.start, circle.center, segment_id]
2585        };
2586        let src_delta = SourceDelta { text: new_source };
2587        // Uses .no_freedom_analysis() so freedom_analysis: false
2588        let outcome = self.update_state_after_exec(outcome, false);
2589        let scene_graph_delta = SceneGraphDelta {
2590            new_graph: self.scene_graph_for_ui(),
2591            invalidates_ids: false,
2592            new_objects: new_object_ids,
2593            exec_outcome: outcome,
2594        };
2595        Ok((src_delta, scene_graph_delta))
2596    }
2597
2598    async fn add_control_point_spline(
2599        &mut self,
2600        ctx: &ExecutorContext,
2601        sketch: ObjectId,
2602        ctor: ControlPointSplineCtor,
2603    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2604        let new_program = ensure_control_point_spline_experimental_features(&self.program)
2605            .map_err(KclErrorWithOutputs::no_outputs)?;
2606
2607        let points_ast = to_ast_point2d_array(&ctor.points)
2608            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2609        let mut arguments = vec![ast::LabeledArg {
2610            label: Some(ast::Identifier::new(CONTROL_POINT_SPLINE_POINTS_PARAM)),
2611            arg: points_ast,
2612        }];
2613        if ctor.construction == Some(true) {
2614            arguments.push(ast::LabeledArg {
2615                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2616                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2617                    value: ast::LiteralValue::Bool(true),
2618                    raw: "true".to_string(),
2619                    digest: None,
2620                }))),
2621            });
2622        }
2623        let spline_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2624            callee: ast::Node::no_src(ast_sketch2_name(CONTROL_POINT_SPLINE_FN)),
2625            unlabeled: None,
2626            arguments,
2627            digest: None,
2628            non_code_meta: Default::default(),
2629        })));
2630
2631        let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
2632            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2633        })?;
2634        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2635            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2636                "Object is not a sketch, it is {}",
2637                sketch_object.kind.human_friendly_kind_with_article(),
2638            ))));
2639        };
2640
2641        let mut new_ast = new_program.ast.clone();
2642        let (sketch_block_ref, _) = self
2643            .mutate_ast(
2644                &mut new_ast,
2645                sketch,
2646                AstMutateCommand::AddSketchBlockExprStmt { expr: spline_ast },
2647            )
2648            .map_err(KclErrorWithOutputs::no_outputs)?;
2649        let new_source = source_from_ast(&new_ast);
2650        let new_program = parse_frontend_mutation_source(
2651            &new_source,
2652            "Error parsing KCL source after adding controlPointSpline",
2653            "No AST produced after adding controlPointSpline",
2654        )?;
2655
2656        let spline_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2657            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2658                "Source range of controlPointSpline not found in sketch block: {sketch_block_ref:?}; {err:?}"
2659            )))
2660        })?;
2661
2662        self.program = new_program.clone();
2663
2664        let mut truncated_program = new_program;
2665        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2666            .map_err(KclErrorWithOutputs::no_outputs)?;
2667
2668        let outcome = ctx
2669            .run_mock(
2670                &truncated_program,
2671                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2672            )
2673            .await?;
2674
2675        let new_object_ids = {
2676            let make_err =
2677                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2678            let segment_id = outcome
2679                .source_range_to_object
2680                .get(&spline_node_ref.range)
2681                .copied()
2682                .ok_or_else(|| {
2683                    make_err(format!(
2684                        "Source range of controlPointSpline not found: {spline_node_ref:?}"
2685                    ))
2686                })?;
2687            let segment_object = outcome
2688                .scene_objects
2689                .get(segment_id.0)
2690                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2691            let ObjectKind::Segment { segment } = &segment_object.kind else {
2692                return Err(make_err(format!(
2693                    "Object is not a segment, it is {}",
2694                    segment_object.kind.human_friendly_kind_with_article()
2695                )));
2696            };
2697            let Segment::ControlPointSpline(spline) = segment else {
2698                return Err(make_err(format!(
2699                    "Segment is not a control point spline, it is {}",
2700                    segment.human_friendly_kind_with_article()
2701                )));
2702            };
2703
2704            let mut ids = outcome
2705                .scene_objects
2706                .iter()
2707                .filter_map(|obj| match &obj.kind {
2708                    ObjectKind::Segment {
2709                        segment: Segment::Line(line),
2710                    } if line.owner == Some(segment_id) => Some(obj.id),
2711                    _ => None,
2712                })
2713                .collect::<Vec<_>>();
2714            ids.extend(spline.controls.clone());
2715            ids.push(segment_id);
2716            ids
2717        };
2718        let src_delta = SourceDelta { text: new_source };
2719        let outcome = self.update_state_after_exec(outcome, false);
2720        let scene_graph_delta = SceneGraphDelta {
2721            new_graph: self.scene_graph_for_ui(),
2722            invalidates_ids: false,
2723            new_objects: new_object_ids,
2724            exec_outcome: outcome,
2725        };
2726        Ok((src_delta, scene_graph_delta))
2727    }
2728
2729    fn edit_point(
2730        &mut self,
2731        new_ast: &mut ast::Node<ast::Program>,
2732        sketch: ObjectId,
2733        point: ObjectId,
2734        ctor: PointCtor,
2735    ) -> Result<(), KclError> {
2736        // Create updated KCL source from args.
2737        let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2738
2739        // Look up existing sketch.
2740        let sketch_id = sketch;
2741        let sketch_object = self
2742            .scene_graph
2743            .objects
2744            .get(sketch_id.0)
2745            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2746        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2747            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2748        };
2749        sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2750            KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2751        })?;
2752        // Look up existing point.
2753        let point_id = point;
2754        let point_object = self
2755            .scene_graph
2756            .objects
2757            .get(point_id.0)
2758            .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2759        let ObjectKind::Segment {
2760            segment: Segment::Point(point),
2761        } = &point_object.kind
2762        else {
2763            return Err(KclError::refactor(format!(
2764                "Object is not a point segment: {point_object:?}"
2765            )));
2766        };
2767
2768        // If the point is part of a line or arc, edit the line/arc instead.
2769        if let Some(owner_id) = point.owner {
2770            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2771                KclError::refactor(format!(
2772                    "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2773                ))
2774            })?;
2775            let ObjectKind::Segment { segment } = &owner_object.kind else {
2776                return Err(KclError::refactor(format!(
2777                    "Internal: Owner of point is not a segment, but found {}",
2778                    owner_object.kind.human_friendly_kind_with_article()
2779                )));
2780            };
2781
2782            // Handle Line owner
2783            if let Segment::Line(line) = segment {
2784                let SegmentCtor::Line(line_ctor) = &line.ctor else {
2785                    return Err(KclError::refactor(format!(
2786                        "Internal: Owner of point does not have line ctor, but found {}",
2787                        line.ctor.human_friendly_kind_with_article()
2788                    )));
2789                };
2790                let mut line_ctor = line_ctor.clone();
2791                // Which end of the line is this point?
2792                if line.start == point_id {
2793                    line_ctor.start = ctor.position;
2794                } else if line.end == point_id {
2795                    line_ctor.end = ctor.position;
2796                } else {
2797                    return Err(KclError::refactor(format!(
2798                        "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2799                    )));
2800                }
2801                return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2802            }
2803
2804            // Handle Arc owner
2805            if let Segment::Arc(arc) = segment {
2806                let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2807                    return Err(KclError::refactor(format!(
2808                        "Internal: Owner of point does not have arc ctor, but found {}",
2809                        arc.ctor.human_friendly_kind_with_article()
2810                    )));
2811                };
2812                let mut arc_ctor = arc_ctor.clone();
2813                // Which point of the arc is this? (center, start, or end)
2814                if arc.center == point_id {
2815                    arc_ctor.center = ctor.position;
2816                } else if arc.start == point_id {
2817                    arc_ctor.start = ctor.position;
2818                } else if arc.end == point_id {
2819                    arc_ctor.end = ctor.position;
2820                } else {
2821                    return Err(KclError::refactor(format!(
2822                        "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2823                    )));
2824                }
2825                return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2826            }
2827
2828            // Handle Circle owner
2829            if let Segment::Circle(circle) = segment {
2830                let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2831                    return Err(KclError::refactor(format!(
2832                        "Internal: Owner of point does not have circle ctor, but found {}",
2833                        circle.ctor.human_friendly_kind_with_article()
2834                    )));
2835                };
2836                let mut circle_ctor = circle_ctor.clone();
2837                if circle.center == point_id {
2838                    circle_ctor.center = ctor.position;
2839                } else if circle.start == point_id {
2840                    circle_ctor.start = ctor.position;
2841                } else {
2842                    return Err(KclError::refactor(format!(
2843                        "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2844                    )));
2845                }
2846                return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2847            }
2848
2849            if let Segment::ControlPointSpline(spline) = segment {
2850                let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor else {
2851                    return Err(KclError::refactor(format!(
2852                        "Internal: Owner of point does not have controlPointSpline ctor, but found {}",
2853                        spline.ctor.human_friendly_kind_with_article()
2854                    )));
2855                };
2856                let mut spline_ctor = spline_ctor.clone();
2857                let Some(control_index) = spline.controls.iter().position(|id| *id == point_id) else {
2858                    return Err(KclError::refactor(format!(
2859                        "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
2860                    )));
2861                };
2862                spline_ctor.points[control_index] = ctor.position;
2863                return self.edit_control_point_spline(new_ast, sketch_id, owner_id, spline_ctor);
2864            }
2865
2866            // If owner is neither Line, Arc, nor Circle, allow editing the point directly
2867            // (fall through to the point editing logic below)
2868        }
2869
2870        // Modify the point AST.
2871        self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2872        Ok(())
2873    }
2874
2875    fn edit_line(
2876        &mut self,
2877        new_ast: &mut ast::Node<ast::Program>,
2878        sketch: ObjectId,
2879        line: ObjectId,
2880        ctor: LineCtor,
2881    ) -> Result<(), KclError> {
2882        // Create updated KCL source from args.
2883        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2884        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2885
2886        // Look up existing sketch.
2887        let sketch_id = sketch;
2888        let sketch_object = self
2889            .scene_graph
2890            .objects
2891            .get(sketch_id.0)
2892            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2893        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2894            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2895        };
2896        sketch
2897            .segments
2898            .iter()
2899            .find(|o| **o == line)
2900            .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2901        // Look up existing line.
2902        let line_id = line;
2903        let line_object = self
2904            .scene_graph
2905            .objects
2906            .get(line_id.0)
2907            .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2908        let ObjectKind::Segment { .. } = &line_object.kind else {
2909            let kind = line_object.kind.human_friendly_kind_with_article();
2910            return Err(KclError::refactor(format!(
2911                "This constraint only works on Segments, but you selected {kind}"
2912            )));
2913        };
2914
2915        // Modify the line AST.
2916        self.mutate_ast(
2917            new_ast,
2918            line_id,
2919            AstMutateCommand::EditLine {
2920                start: new_start_ast,
2921                end: new_end_ast,
2922                construction: ctor.construction,
2923            },
2924        )?;
2925        Ok(())
2926    }
2927
2928    fn edit_arc(
2929        &mut self,
2930        new_ast: &mut ast::Node<ast::Program>,
2931        sketch: ObjectId,
2932        arc: ObjectId,
2933        ctor: ArcCtor,
2934    ) -> Result<(), KclError> {
2935        // Create updated KCL source from args.
2936        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2937        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2938        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2939
2940        // Look up existing sketch.
2941        let sketch_id = sketch;
2942        let sketch_object = self
2943            .scene_graph
2944            .objects
2945            .get(sketch_id.0)
2946            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2947        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2948            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2949        };
2950        sketch
2951            .segments
2952            .iter()
2953            .find(|o| **o == arc)
2954            .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2955        // Look up existing arc.
2956        let arc_id = arc;
2957        let arc_object = self
2958            .scene_graph
2959            .objects
2960            .get(arc_id.0)
2961            .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2962        let ObjectKind::Segment { .. } = &arc_object.kind else {
2963            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2964        };
2965
2966        // Modify the arc AST.
2967        self.mutate_ast(
2968            new_ast,
2969            arc_id,
2970            AstMutateCommand::EditArc {
2971                start: new_start_ast,
2972                end: new_end_ast,
2973                center: new_center_ast,
2974                direction: ctor.direction,
2975                construction: ctor.construction,
2976            },
2977        )?;
2978        Ok(())
2979    }
2980
2981    fn edit_circle(
2982        &mut self,
2983        new_ast: &mut ast::Node<ast::Program>,
2984        sketch: ObjectId,
2985        circle: ObjectId,
2986        ctor: CircleCtor,
2987    ) -> Result<(), KclError> {
2988        // Create updated KCL source from args.
2989        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2990        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2991
2992        // Look up existing sketch.
2993        let sketch_id = sketch;
2994        let sketch_object = self
2995            .scene_graph
2996            .objects
2997            .get(sketch_id.0)
2998            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2999        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3000            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3001        };
3002        sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
3003            KclError::refactor(format!(
3004                "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
3005            ))
3006        })?;
3007        // Look up existing circle.
3008        let circle_id = circle;
3009        let circle_object = self
3010            .scene_graph
3011            .objects
3012            .get(circle_id.0)
3013            .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
3014        let ObjectKind::Segment { .. } = &circle_object.kind else {
3015            return Err(KclError::refactor(format!(
3016                "Object is not a segment: {circle_object:?}"
3017            )));
3018        };
3019
3020        // Modify the circle AST.
3021        self.mutate_ast(
3022            new_ast,
3023            circle_id,
3024            AstMutateCommand::EditCircle {
3025                start: new_start_ast,
3026                center: new_center_ast,
3027                construction: ctor.construction,
3028            },
3029        )?;
3030        Ok(())
3031    }
3032
3033    fn edit_control_point_spline(
3034        &mut self,
3035        new_ast: &mut ast::Node<ast::Program>,
3036        sketch: ObjectId,
3037        spline: ObjectId,
3038        ctor: ControlPointSplineCtor,
3039    ) -> Result<(), KclError> {
3040        let points_ast = to_ast_point2d_array(&ctor.points).map_err(|err| KclError::refactor(err.to_string()))?;
3041
3042        let sketch_object = self
3043            .scene_graph
3044            .objects
3045            .get(sketch.0)
3046            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3047        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3048            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3049        };
3050        sketch.segments.iter().find(|o| **o == spline).ok_or_else(|| {
3051            KclError::refactor(format!(
3052                "Control point spline not found in sketch: spline={spline:?}, sketch={sketch:?}"
3053            ))
3054        })?;
3055
3056        let spline_object =
3057            self.scene_graph.objects.get(spline.0).ok_or_else(|| {
3058                KclError::refactor(format!("Control point spline not found in scene graph: {spline:?}"))
3059            })?;
3060        let ObjectKind::Segment { .. } = &spline_object.kind else {
3061            return Err(KclError::refactor(format!(
3062                "Object is not a segment: {spline_object:?}"
3063            )));
3064        };
3065
3066        self.mutate_ast(
3067            new_ast,
3068            spline,
3069            AstMutateCommand::EditControlPointSpline {
3070                points: points_ast,
3071                construction: ctor.construction,
3072            },
3073        )?;
3074        Ok(())
3075    }
3076
3077    fn delete_segment(
3078        &mut self,
3079        new_ast: &mut ast::Node<ast::Program>,
3080        sketch: ObjectId,
3081        segment_id: ObjectId,
3082    ) -> Result<(), KclError> {
3083        // Look up existing sketch.
3084        let sketch_id = sketch;
3085        let sketch_object = self
3086            .scene_graph
3087            .objects
3088            .get(sketch_id.0)
3089            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3090        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3091            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3092        };
3093        sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
3094            KclError::refactor(format!(
3095                "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
3096            ))
3097        })?;
3098        // Look up existing segment.
3099        let segment_object =
3100            self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
3101                KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
3102            })?;
3103        let ObjectKind::Segment { .. } = &segment_object.kind else {
3104            return Err(KclError::refactor(format!(
3105                "Object is not a segment, it is {}",
3106                segment_object.kind.human_friendly_kind_with_article()
3107            )));
3108        };
3109
3110        // Modify the AST to remove the segment.
3111        self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
3112        Ok(())
3113    }
3114
3115    fn delete_constraint(
3116        &mut self,
3117        new_ast: &mut ast::Node<ast::Program>,
3118        sketch: ObjectId,
3119        constraint_id: ObjectId,
3120    ) -> Result<(), KclError> {
3121        // Look up existing sketch.
3122        let sketch_id = sketch;
3123        let sketch_object = self
3124            .scene_graph
3125            .objects
3126            .get(sketch_id.0)
3127            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3128        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3129            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3130        };
3131        sketch
3132            .constraints
3133            .iter()
3134            .find(|o| **o == constraint_id)
3135            .ok_or_else(|| {
3136                KclError::refactor(format!(
3137                    "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
3138                ))
3139            })?;
3140        // Look up existing constraint.
3141        let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
3142            KclError::refactor(format!(
3143                "Constraint not found in scene graph: constraint={constraint_id:?}"
3144            ))
3145        })?;
3146        let ObjectKind::Constraint { .. } = &constraint_object.kind else {
3147            return Err(KclError::refactor(format!(
3148                "Object is not a constraint, it is {}",
3149                constraint_object.kind.human_friendly_kind_with_article()
3150            )));
3151        };
3152
3153        // Modify the AST to remove the constraint.
3154        self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
3155        Ok(())
3156    }
3157
3158    fn edit_coincident_constraint(
3159        &mut self,
3160        new_ast: &mut ast::Node<ast::Program>,
3161        constraint_id: ObjectId,
3162        segments: Vec<ConstraintSegment>,
3163    ) -> Result<(), KclError> {
3164        if segments.len() < 2 {
3165            return Err(KclError::refactor(format!(
3166                "Coincident constraint must have at least 2 inputs, got {}",
3167                segments.len()
3168            )));
3169        }
3170
3171        let segment_asts = segments
3172            .iter()
3173            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3174            .collect::<Result<Vec<_>, _>>()?;
3175
3176        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3177            elements: segment_asts,
3178            digest: None,
3179            non_code_meta: Default::default(),
3180        })));
3181
3182        self.mutate_ast(
3183            new_ast,
3184            constraint_id,
3185            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3186        )?;
3187        Ok(())
3188    }
3189
3190    fn edit_horizontal_points_constraint(
3191        &mut self,
3192        new_ast: &mut ast::Node<ast::Program>,
3193        constraint_id: ObjectId,
3194        points: Vec<ConstraintSegment>,
3195    ) -> Result<(), KclError> {
3196        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
3197    }
3198
3199    fn edit_vertical_points_constraint(
3200        &mut self,
3201        new_ast: &mut ast::Node<ast::Program>,
3202        constraint_id: ObjectId,
3203        points: Vec<ConstraintSegment>,
3204    ) -> Result<(), KclError> {
3205        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
3206    }
3207
3208    fn edit_axis_points_constraint(
3209        &mut self,
3210        new_ast: &mut ast::Node<ast::Program>,
3211        constraint_id: ObjectId,
3212        points: Vec<ConstraintSegment>,
3213        constraint_name: &str,
3214    ) -> Result<(), KclError> {
3215        if points.len() < 2 {
3216            return Err(KclError::refactor(format!(
3217                "{constraint_name} points constraint must have at least 2 points, got {}",
3218                points.len()
3219            )));
3220        }
3221
3222        let point_asts = points
3223            .iter()
3224            .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3225            .collect::<Result<Vec<_>, _>>()?;
3226
3227        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3228            elements: point_asts,
3229            digest: None,
3230            non_code_meta: Default::default(),
3231        })));
3232
3233        self.mutate_ast(
3234            new_ast,
3235            constraint_id,
3236            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3237        )?;
3238        Ok(())
3239    }
3240
3241    /// updates the equalLength constraint with the given lines
3242    fn edit_equal_length_constraint(
3243        &mut self,
3244        new_ast: &mut ast::Node<ast::Program>,
3245        constraint_id: ObjectId,
3246        lines: Vec<ObjectId>,
3247    ) -> Result<(), KclError> {
3248        if lines.len() < 2 {
3249            return Err(KclError::refactor(format!(
3250                "Lines equal length constraint must have at least 2 lines, got {}",
3251                lines.len()
3252            )));
3253        }
3254
3255        let line_asts = lines
3256            .iter()
3257            .map(|line_id| {
3258                let line_object = self
3259                    .scene_graph
3260                    .objects
3261                    .get(line_id.0)
3262                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3263                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3264                    let kind = line_object.kind.human_friendly_kind_with_article();
3265                    return Err(KclError::refactor(format!(
3266                        "This constraint only works on Segments, but you selected {kind}"
3267                    )));
3268                };
3269                let Segment::Line(_) = line_segment else {
3270                    let kind = line_segment.human_friendly_kind_with_article();
3271                    return Err(KclError::refactor(format!(
3272                        "Only lines can be made equal length, but you selected {kind}"
3273                    )));
3274                };
3275
3276                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3277            })
3278            .collect::<Result<Vec<_>, _>>()?;
3279
3280        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3281            elements: line_asts,
3282            digest: None,
3283            non_code_meta: Default::default(),
3284        })));
3285
3286        self.mutate_ast(
3287            new_ast,
3288            constraint_id,
3289            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3290        )?;
3291        Ok(())
3292    }
3293
3294    /// Updates the parallel constraint with the given lines.
3295    fn edit_parallel_constraint(
3296        &mut self,
3297        new_ast: &mut ast::Node<ast::Program>,
3298        constraint_id: ObjectId,
3299        lines: Vec<ObjectId>,
3300    ) -> Result<(), KclError> {
3301        if lines.len() < 2 {
3302            return Err(KclError::refactor(format!(
3303                "Parallel constraint must have at least 2 lines, got {}",
3304                lines.len()
3305            )));
3306        }
3307
3308        let line_asts = lines
3309            .iter()
3310            .map(|line_id| {
3311                let line_object = self
3312                    .scene_graph
3313                    .objects
3314                    .get(line_id.0)
3315                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3316                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3317                    let kind = line_object.kind.human_friendly_kind_with_article();
3318                    return Err(KclError::refactor(format!(
3319                        "This constraint only works on Segments, but you selected {kind}"
3320                    )));
3321                };
3322                let Segment::Line(_) = line_segment else {
3323                    let kind = line_segment.human_friendly_kind_with_article();
3324                    return Err(KclError::refactor(format!(
3325                        "Only lines can be made parallel, but you selected {kind}"
3326                    )));
3327                };
3328
3329                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3330            })
3331            .collect::<Result<Vec<_>, _>>()?;
3332
3333        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3334            elements: line_asts,
3335            digest: None,
3336            non_code_meta: Default::default(),
3337        })));
3338
3339        self.mutate_ast(
3340            new_ast,
3341            constraint_id,
3342            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3343        )?;
3344        Ok(())
3345    }
3346
3347    /// Updates the equalRadius constraint with the given segments.
3348    fn edit_equal_radius_constraint(
3349        &mut self,
3350        new_ast: &mut ast::Node<ast::Program>,
3351        constraint_id: ObjectId,
3352        input: Vec<ObjectId>,
3353    ) -> Result<(), KclError> {
3354        if input.len() < 2 {
3355            return Err(KclError::refactor(format!(
3356                "equalRadius constraint must have at least 2 segments, got {}",
3357                input.len()
3358            )));
3359        }
3360
3361        let input_asts = input
3362            .iter()
3363            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3364            .collect::<Result<Vec<_>, _>>()?;
3365
3366        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3367            elements: input_asts,
3368            digest: None,
3369            non_code_meta: Default::default(),
3370        })));
3371
3372        self.mutate_ast(
3373            new_ast,
3374            constraint_id,
3375            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3376        )?;
3377        Ok(())
3378    }
3379
3380    async fn execute_after_edit(
3381        &mut self,
3382        ctx: &ExecutorContext,
3383        sketch: ObjectId,
3384        sketch_block_ref: AstNodeRef,
3385        new_ast: &mut ast::Node<ast::Program>,
3386        options: ExecuteAfterEditOptions,
3387    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3388        let ExecuteAfterEditOptions {
3389            segment_ids_edited,
3390            edit_kind,
3391            commit_solved_initial_guesses,
3392        } = options;
3393
3394        // Convert to string source to create real source ranges.
3395        let new_source = source_from_ast(new_ast);
3396        // Parse the new KCL source.
3397        let new_program = parse_frontend_mutation_source(
3398            &new_source,
3399            "Error parsing KCL source after editing",
3400            "No AST produced after editing",
3401        )?;
3402
3403        // Truncate after the sketch block for mock execution.
3404        let is_delete = edit_kind.is_delete();
3405        let truncated_program = {
3406            let mut truncated_program = new_program.clone();
3407            only_sketch_block(
3408                &mut truncated_program.ast,
3409                &sketch_block_ref,
3410                edit_kind.to_change_kind(),
3411            )
3412            .map_err(KclErrorWithOutputs::no_outputs)?;
3413            truncated_program
3414        };
3415
3416        // Execute.
3417        let drag_anchors = self.next_segment_drag_anchors.take().unwrap_or_default();
3418        let mock_config = MockConfig {
3419            sketch_block_id: Some(sketch),
3420            freedom_analysis: is_delete,
3421            segment_ids_edited: segment_ids_edited.clone(),
3422            drag_anchors,
3423            ..Default::default()
3424        };
3425        let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
3426
3427        // Only now, after execution has succeeded, update self.program.
3428        self.program = new_program;
3429
3430        // Uses freedom_analysis: is_delete
3431        let outcome = self.update_state_after_exec(outcome, is_delete);
3432
3433        let src_delta = if commit_solved_initial_guesses {
3434            self.commit_var_solutions_to_program(&outcome, "editing")?
3435        } else {
3436            SourceDelta { text: new_source }
3437        };
3438        let scene_graph_delta = SceneGraphDelta {
3439            new_graph: self.scene_graph_for_ui(),
3440            invalidates_ids: is_delete,
3441            new_objects: Vec::new(),
3442            exec_outcome: outcome,
3443        };
3444        Ok((src_delta, scene_graph_delta))
3445    }
3446
3447    async fn execute_after_delete_sketch(
3448        &mut self,
3449        ctx: &ExecutorContext,
3450        new_ast: &mut ast::Node<ast::Program>,
3451    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3452        // Convert to string source to create real source ranges.
3453        let new_source = source_from_ast(new_ast);
3454        // Parse the new KCL source.
3455        let new_program = parse_frontend_mutation_source(
3456            &new_source,
3457            "Error parsing KCL source after editing",
3458            "No AST produced after editing",
3459        )?;
3460
3461        // Make sure to only set this if there are no errors.
3462        self.program = new_program.clone();
3463
3464        // We deleted the entire sketch block. It doesn't make sense to truncate
3465        // and execute only the sketch block. We execute the whole program with
3466        // a real engine.
3467
3468        // Execute.
3469        let outcome = ctx.run_with_caching(new_program).await?;
3470        let freedom_analysis_ran = true;
3471
3472        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
3473
3474        let src_delta = SourceDelta { text: new_source };
3475        let scene_graph_delta = SceneGraphDelta {
3476            new_graph: self.scene_graph_for_ui(),
3477            invalidates_ids: true,
3478            new_objects: Vec::new(),
3479            exec_outcome: outcome,
3480        };
3481        Ok((src_delta, scene_graph_delta))
3482    }
3483
3484    /// Map a point object id into an AST reference expression for use in
3485    /// constraints. If the point is owned by a segment (line or arc), we
3486    /// reference the appropriate property on that segment (e.g. `line1.start`,
3487    /// `arc1.center`). Otherwise we reference the point directly.
3488    fn point_id_to_ast_reference(
3489        &self,
3490        point_id: ObjectId,
3491        new_ast: &mut ast::Node<ast::Program>,
3492    ) -> Result<ast::Expr, KclError> {
3493        let point_object = self
3494            .scene_graph
3495            .objects
3496            .get(point_id.0)
3497            .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
3498        let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
3499            return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
3500        };
3501        let Segment::Point(point) = point_segment else {
3502            return Err(KclError::refactor(format!(
3503                "Only points are currently supported: {point_object:?}"
3504            )));
3505        };
3506
3507        if let Some(owner_id) = point.owner {
3508            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3509                KclError::refactor(format!(
3510                    "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
3511                ))
3512            })?;
3513            let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3514                return Err(KclError::refactor(format!(
3515                    "Owner of point is not a segment, but found {}",
3516                    owner_object.kind.human_friendly_kind_with_article()
3517                )));
3518            };
3519
3520            match owner_segment {
3521                Segment::Line(line) => {
3522                    let property = if line.start == point_id {
3523                        LINE_PROPERTY_START
3524                    } else if line.end == point_id {
3525                        LINE_PROPERTY_END
3526                    } else {
3527                        return Err(KclError::refactor(format!(
3528                            "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
3529                        )));
3530                    };
3531                    get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
3532                }
3533                Segment::Arc(arc) => {
3534                    let property = if arc.start == point_id {
3535                        ARC_PROPERTY_START
3536                    } else if arc.end == point_id {
3537                        ARC_PROPERTY_END
3538                    } else if arc.center == point_id {
3539                        ARC_PROPERTY_CENTER
3540                    } else {
3541                        return Err(KclError::refactor(format!(
3542                            "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
3543                        )));
3544                    };
3545                    get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
3546                }
3547                Segment::Circle(circle) => {
3548                    let property = if circle.start == point_id {
3549                        CIRCLE_PROPERTY_START
3550                    } else if circle.center == point_id {
3551                        CIRCLE_PROPERTY_CENTER
3552                    } else {
3553                        return Err(KclError::refactor(format!(
3554                            "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
3555                        )));
3556                    };
3557                    get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
3558                }
3559                Segment::ControlPointSpline(spline) => {
3560                    let Some(index) = spline.controls.iter().position(|id| *id == point_id) else {
3561                        return Err(KclError::refactor(format!(
3562                            "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
3563                        )));
3564                    };
3565                    let owner_expr =
3566                        get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3567                    let controls_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_CONTROLS);
3568                    Ok(create_index_expression(controls_expr, index))
3569                }
3570                _ => Err(KclError::refactor(format!(
3571                    "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
3572                ))),
3573            }
3574        } else {
3575            // Standalone point.
3576            get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
3577        }
3578    }
3579
3580    fn line_id_to_ast_reference(
3581        &self,
3582        line_id: ObjectId,
3583        new_ast: &mut ast::Node<ast::Program>,
3584    ) -> Result<ast::Expr, KclError> {
3585        let line_object = self
3586            .scene_graph
3587            .objects
3588            .get(line_id.0)
3589            .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3590        let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3591            return Err(KclError::refactor(format!("Object is not a segment: {line_object:?}")));
3592        };
3593        let Segment::Line(line) = line_segment else {
3594            return Err(KclError::refactor(format!(
3595                "Only lines are currently supported: {line_object:?}"
3596            )));
3597        };
3598
3599        if let Some(owner_id) = line.owner {
3600            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3601                KclError::refactor(format!(
3602                    "Owner of line not found in scene graph: line={line_id:?}, owner={owner_id:?}"
3603                ))
3604            })?;
3605            let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3606                return Err(KclError::refactor(format!(
3607                    "Owner of line is not a segment, but found {}",
3608                    owner_object.kind.human_friendly_kind_with_article()
3609                )));
3610            };
3611
3612            match owner_segment {
3613                Segment::ControlPointSpline(spline) => {
3614                    let Some(index) = spline
3615                        .controls
3616                        .windows(2)
3617                        .position(|window| window[0] == line.start && window[1] == line.end)
3618                    else {
3619                        return Err(KclError::refactor(format!(
3620                            "Internal: Line is not part of owner's controlPointSpline segment: line={line_id:?}, spline={owner_id:?}"
3621                        )));
3622                    };
3623                    let owner_expr =
3624                        get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3625                    let edges_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_EDGES);
3626                    Ok(create_index_expression(edges_expr, index))
3627                }
3628                _ => Err(KclError::refactor(format!(
3629                    "Internal: Owner of line is not a supported segment type for constraints: {owner_segment:?}"
3630                ))),
3631            }
3632        } else {
3633            get_or_insert_ast_reference(new_ast, &line_object.source, "line", None)
3634        }
3635    }
3636
3637    fn coincident_segment_to_ast(
3638        &self,
3639        segment: &ConstraintSegment,
3640        new_ast: &mut ast::Node<ast::Program>,
3641    ) -> Result<ast::Expr, KclError> {
3642        match segment {
3643            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3644            ConstraintSegment::Segment(segment_id) => self.segment_id_to_constraint_ast_reference(*segment_id, new_ast),
3645        }
3646    }
3647
3648    fn segment_id_to_constraint_ast_reference(
3649        &self,
3650        segment_id: ObjectId,
3651        new_ast: &mut ast::Node<ast::Program>,
3652    ) -> Result<ast::Expr, KclError> {
3653        let segment_object = self
3654            .scene_graph
3655            .objects
3656            .get(segment_id.0)
3657            .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
3658        let ObjectKind::Segment { segment } = &segment_object.kind else {
3659            return Err(KclError::refactor(format!(
3660                "Object is not a segment, it is {}",
3661                segment_object.kind.human_friendly_kind_with_article()
3662            )));
3663        };
3664
3665        match segment {
3666            Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3667            Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
3668            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None),
3669            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3670            Segment::ControlPointSpline(_) => {
3671                get_or_insert_ast_reference(new_ast, &segment_object.source, CONTROL_POINT_SPLINE_FN, None)
3672            }
3673        }
3674    }
3675
3676    fn axis_constraint_segment_to_ast(
3677        &self,
3678        segment: &ConstraintSegment,
3679        new_ast: &mut ast::Node<ast::Program>,
3680    ) -> Result<ast::Expr, KclError> {
3681        match segment {
3682            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3683            ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3684        }
3685    }
3686
3687    async fn add_coincident(
3688        &mut self,
3689        sketch: ObjectId,
3690        coincident: Coincident,
3691        new_ast: &mut ast::Node<ast::Program>,
3692    ) -> Result<AstNodeRef, KclError> {
3693        let sketch_id = sketch;
3694        for segment in &coincident.segments {
3695            let ConstraintSegment::Segment(segment_id) = segment else {
3696                continue;
3697            };
3698            let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
3699                continue;
3700            };
3701            if matches!(
3702                segment_object.kind,
3703                ObjectKind::Segment {
3704                    segment: Segment::ControlPointSpline(_)
3705                }
3706            ) {
3707                return Err(KclError::refactor(
3708                    "Coincident with a full controlPointSpline is not supported yet. Constrain a control point or spline edge instead."
3709                        .to_owned(),
3710                ));
3711            }
3712        }
3713        let segment_asts = coincident
3714            .segments
3715            .iter()
3716            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3717            .collect::<Result<Vec<_>, _>>()?;
3718        if segment_asts.len() < 2 {
3719            return Err(KclError::refactor(format!(
3720                "Coincident constraint must have at least 2 inputs, got {}",
3721                segment_asts.len()
3722            )));
3723        }
3724
3725        // Create the coincident() call using shared helper.
3726        let coincident_ast = create_coincident_ast(segment_asts);
3727
3728        // Add the line to the AST of the sketch block.
3729        let (sketch_block_ref, _) = self.mutate_ast(
3730            new_ast,
3731            sketch_id,
3732            AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3733        )?;
3734        Ok(sketch_block_ref)
3735    }
3736
3737    async fn add_distance(
3738        &mut self,
3739        sketch: ObjectId,
3740        distance: Distance,
3741        new_ast: &mut ast::Node<ast::Program>,
3742    ) -> Result<AstNodeRef, KclError> {
3743        self.add_distance_constraint(sketch, DISTANCE_FN, distance, new_ast)
3744    }
3745
3746    fn distance_constraint_ast_parts(
3747        &self,
3748        function_name: &str,
3749        distance: &Distance,
3750        new_ast: &mut ast::Node<ast::Program>,
3751    ) -> Result<(ast::BinaryPart, ast::BinaryPart), KclError> {
3752        let [segment0_ast, segment1_ast] = match distance.segments.as_slice() {
3753            [pt0, pt1] => [
3754                self.coincident_segment_to_ast(pt0, new_ast)?,
3755                self.coincident_segment_to_ast(pt1, new_ast)?,
3756            ],
3757            _ => {
3758                return Err(KclError::refactor(format!(
3759                    "Distance constraint must have exactly 2 segments, got {}",
3760                    distance.segments.len()
3761                )));
3762            }
3763        };
3764
3765        let arguments = match &distance.label_position {
3766            Some(label_position) => vec![ast::LabeledArg {
3767                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3768                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3769            }],
3770            None => Default::default(),
3771        };
3772
3773        let call = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
3774            callee: ast::Node::no_src(ast_sketch2_name(function_name)),
3775            unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
3776                ast::ArrayExpression {
3777                    elements: vec![segment0_ast, segment1_ast],
3778                    digest: None,
3779                    non_code_meta: Default::default(),
3780                },
3781            )))),
3782            arguments,
3783            digest: None,
3784            non_code_meta: Default::default(),
3785        })));
3786        let value = ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3787            value: ast::LiteralValue::Number {
3788                value: distance.distance.value,
3789                suffix: distance.distance.units,
3790            },
3791            raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3792                KclError::refactor(format!(
3793                    "Could not format numeric suffix: {:?}",
3794                    distance.distance.units
3795                ))
3796            })?,
3797            digest: None,
3798        })));
3799
3800        Ok((call, value))
3801    }
3802
3803    fn add_distance_constraint(
3804        &mut self,
3805        sketch: ObjectId,
3806        function_name: &str,
3807        distance: Distance,
3808        new_ast: &mut ast::Node<ast::Program>,
3809    ) -> Result<AstNodeRef, KclError> {
3810        let (call, value) = self.distance_constraint_ast_parts(function_name, &distance, new_ast)?;
3811        let distance_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
3812            left: call,
3813            operator: ast::BinaryOperator::Eq,
3814            right: value,
3815            digest: None,
3816        })));
3817
3818        let (sketch_block_ref, _) = self.mutate_ast(
3819            new_ast,
3820            sketch,
3821            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3822        )?;
3823        Ok(sketch_block_ref)
3824    }
3825
3826    async fn add_angle(
3827        &mut self,
3828        sketch: ObjectId,
3829        angle: Angle,
3830        new_ast: &mut ast::Node<ast::Program>,
3831    ) -> Result<AstNodeRef, KclError> {
3832        let sketch_id = sketch;
3833        let (angle_call_ast, angle_value_ast) = self.angle_constraint_ast_parts(&angle, new_ast)?;
3834        let angle_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
3835            left: angle_call_ast,
3836            operator: ast::BinaryOperator::Eq,
3837            right: angle_value_ast,
3838            digest: None,
3839        })));
3840
3841        // Add the line to the AST of the sketch block.
3842        let (sketch_block_ref, _) = self.mutate_ast(
3843            new_ast,
3844            sketch_id,
3845            AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3846        )?;
3847        Ok(sketch_block_ref)
3848    }
3849
3850    fn angle_constraint_ast_parts(
3851        &self,
3852        angle: &Angle,
3853        new_ast: &mut ast::Node<ast::Program>,
3854    ) -> Result<(ast::BinaryPart, ast::BinaryPart), KclError> {
3855        let &[l0_id, l1_id] = angle.lines.as_slice() else {
3856            return Err(KclError::refactor(format!(
3857                "Angle constraint must have exactly 2 lines, got {}",
3858                angle.lines.len()
3859            )));
3860        };
3861
3862        let l0_ast = self.line_id_to_ast_reference(l0_id, new_ast)?;
3863        let l1_ast = self.line_id_to_ast_reference(l1_id, new_ast)?;
3864        let lines_ast = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3865            elements: vec![l0_ast, l1_ast],
3866            digest: None,
3867            non_code_meta: Default::default(),
3868        })));
3869
3870        if angle.inverse == Some(true) && angle.sector.is_none() {
3871            return Err(KclError::refactor("Angle inverse requires an angle sector".to_owned()));
3872        }
3873
3874        let uses_angle_dimension = angle.sector.is_some();
3875        let mut arguments = if uses_angle_dimension {
3876            vec![ast::LabeledArg {
3877                label: Some(ast::Identifier::new(ANGLE_LINES_PARAM)),
3878                arg: lines_ast.clone(),
3879            }]
3880        } else {
3881            Default::default()
3882        };
3883
3884        if let Some(sector) = angle.sector {
3885            arguments.push(ast::LabeledArg {
3886                label: Some(ast::Identifier::new(ANGLE_SECTOR_PARAM)),
3887                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3888                    value: ast::LiteralValue::Number {
3889                        value: f64::from(sector),
3890                        suffix: NumericSuffix::None,
3891                    },
3892                    raw: sector.to_string(),
3893                    digest: None,
3894                }))),
3895            });
3896        }
3897
3898        if angle.inverse == Some(true) {
3899            arguments.push(ast::LabeledArg {
3900                label: Some(ast::Identifier::new(ANGLE_INVERSE_PARAM)),
3901                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3902                    value: ast::LiteralValue::Bool(true),
3903                    raw: true.to_string(),
3904                    digest: None,
3905                }))),
3906            });
3907        }
3908
3909        if let Some(label_position) = &angle.label_position {
3910            arguments.push(ast::LabeledArg {
3911                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3912                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3913            });
3914        }
3915
3916        let call = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
3917            callee: ast::Node::no_src(ast_sketch2_name(if uses_angle_dimension {
3918                ANGLE_DIMENSION_FN
3919            } else {
3920                ANGLE_FN
3921            })),
3922            unlabeled: (!uses_angle_dimension).then_some(lines_ast),
3923            arguments,
3924            digest: None,
3925            non_code_meta: Default::default(),
3926        })));
3927        let value = ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3928            value: ast::LiteralValue::Number {
3929                value: angle.angle.value,
3930                suffix: angle.angle.units,
3931            },
3932            raw: format_number_literal(angle.angle.value, angle.angle.units, None)
3933                .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units)))?,
3934            digest: None,
3935        })));
3936
3937        Ok((call, value))
3938    }
3939
3940    async fn add_tangent(
3941        &mut self,
3942        sketch: ObjectId,
3943        tangent: Tangent,
3944        new_ast: &mut ast::Node<ast::Program>,
3945    ) -> Result<AstNodeRef, KclError> {
3946        let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3947            return Err(KclError::refactor(format!(
3948                "Tangent constraint must have exactly 2 segments, got {}",
3949                tangent.input.len()
3950            )));
3951        };
3952        let sketch_id = sketch;
3953
3954        let seg0_object = self
3955            .scene_graph
3956            .objects
3957            .get(seg0_id.0)
3958            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3959        let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3960            return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3961        };
3962        let seg0_ast = match seg0_segment {
3963            Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3964                self.segment_id_to_constraint_ast_reference(seg0_id, new_ast)?
3965            }
3966            _ => {
3967                return Err(KclError::refactor(format!(
3968                    "Tangent supports only line/arc/circle segments for now, got: {seg0_segment:?}"
3969                )));
3970            }
3971        };
3972
3973        let seg1_object = self
3974            .scene_graph
3975            .objects
3976            .get(seg1_id.0)
3977            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3978        let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3979            return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3980        };
3981        let seg1_ast = match seg1_segment {
3982            Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3983                self.segment_id_to_constraint_ast_reference(seg1_id, new_ast)?
3984            }
3985            _ => {
3986                return Err(KclError::refactor(format!(
3987                    "Tangent supports only line/arc/circle segments for now, got: {seg1_segment:?}"
3988                )));
3989            }
3990        };
3991
3992        let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
3993        let (sketch_block_ref, _) = self.mutate_ast(
3994            new_ast,
3995            sketch_id,
3996            AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
3997        )?;
3998        Ok(sketch_block_ref)
3999    }
4000
4001    async fn add_symmetric(
4002        &mut self,
4003        sketch: ObjectId,
4004        symmetric: Symmetric,
4005        new_ast: &mut ast::Node<ast::Program>,
4006    ) -> Result<AstNodeRef, KclError> {
4007        let &[input0_id, input1_id] = symmetric.input.as_slice() else {
4008            return Err(KclError::refactor(format!(
4009                "Symmetric constraint must have exactly 2 inputs, got {}",
4010                symmetric.input.len()
4011            )));
4012        };
4013        let sketch_id = sketch;
4014
4015        let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
4016        let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
4017        let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
4018
4019        let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
4020        let (sketch_block_ref, _) = self.mutate_ast(
4021            new_ast,
4022            sketch_id,
4023            AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
4024        )?;
4025        Ok(sketch_block_ref)
4026    }
4027
4028    async fn add_midpoint(
4029        &mut self,
4030        sketch: ObjectId,
4031        midpoint: Midpoint,
4032        new_ast: &mut ast::Node<ast::Program>,
4033    ) -> Result<AstNodeRef, KclError> {
4034        let sketch_id = sketch;
4035        let point_ast = self.axis_constraint_segment_to_ast(&midpoint.point, new_ast)?;
4036
4037        let segment_object = self
4038            .scene_graph
4039            .objects
4040            .get(midpoint.segment.0)
4041            .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
4042        let ObjectKind::Segment {
4043            segment: midpoint_segment,
4044        } = &segment_object.kind
4045        else {
4046            return Err(KclError::refactor(format!(
4047                "Object must be a segment, but it was {}",
4048                segment_object.kind.human_friendly_kind_with_article()
4049            )));
4050        };
4051        let segment_ast = match midpoint_segment {
4052            Segment::Line(_) => self.line_id_to_ast_reference(midpoint.segment, new_ast)?,
4053            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
4054            _ => {
4055                return Err(KclError::refactor(format!(
4056                    "Midpoint target must be a line or arc segment but it was {}",
4057                    midpoint_segment.human_friendly_kind_with_article()
4058                )));
4059            }
4060        };
4061
4062        let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
4063        let (sketch_block_ref, _) = self.mutate_ast(
4064            new_ast,
4065            sketch_id,
4066            AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
4067        )?;
4068        Ok(sketch_block_ref)
4069    }
4070
4071    async fn add_equal_radius(
4072        &mut self,
4073        sketch: ObjectId,
4074        equal_radius: EqualRadius,
4075        new_ast: &mut ast::Node<ast::Program>,
4076    ) -> Result<AstNodeRef, KclError> {
4077        if equal_radius.input.len() < 2 {
4078            return Err(KclError::refactor(format!(
4079                "equalRadius constraint must have at least 2 segments, got {}",
4080                equal_radius.input.len()
4081            )));
4082        }
4083
4084        let sketch_id = sketch;
4085        let input_asts = equal_radius
4086            .input
4087            .iter()
4088            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
4089            .collect::<Result<Vec<_>, _>>()?;
4090
4091        let equal_radius_ast = create_equal_radius_ast(input_asts);
4092        let (sketch_block_ref, _) = self.mutate_ast(
4093            new_ast,
4094            sketch_id,
4095            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
4096        )?;
4097        Ok(sketch_block_ref)
4098    }
4099
4100    async fn add_radius(
4101        &mut self,
4102        sketch: ObjectId,
4103        radius: Radius,
4104        new_ast: &mut ast::Node<ast::Program>,
4105    ) -> Result<AstNodeRef, KclError> {
4106        let params = ArcSizeConstraintParams {
4107            points: vec![radius.arc],
4108            function_name: RADIUS_FN,
4109            value: radius.radius.value,
4110            units: radius.radius.units,
4111            label_position: radius.label_position,
4112            constraint_type_name: "Radius",
4113        };
4114        self.add_arc_size_constraint(sketch, params, new_ast).await
4115    }
4116
4117    async fn add_diameter(
4118        &mut self,
4119        sketch: ObjectId,
4120        diameter: Diameter,
4121        new_ast: &mut ast::Node<ast::Program>,
4122    ) -> Result<AstNodeRef, KclError> {
4123        let params = ArcSizeConstraintParams {
4124            points: vec![diameter.arc],
4125            function_name: DIAMETER_FN,
4126            value: diameter.diameter.value,
4127            units: diameter.diameter.units,
4128            label_position: diameter.label_position,
4129            constraint_type_name: "Diameter",
4130        };
4131        self.add_arc_size_constraint(sketch, params, new_ast).await
4132    }
4133
4134    async fn add_fixed_constraints(
4135        &mut self,
4136        sketch: ObjectId,
4137        points: Vec<FixedPoint>,
4138        new_ast: &mut ast::Node<ast::Program>,
4139    ) -> Result<AstNodeRef, KclError> {
4140        let mut sketch_block_ref = None;
4141
4142        for fixed_point in points {
4143            let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
4144            let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
4145                .map_err(|err| KclError::refactor(err.to_string()))?;
4146
4147            let (sketch_ref, _) = self.mutate_ast(
4148                new_ast,
4149                sketch,
4150                AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
4151            )?;
4152            sketch_block_ref = Some(sketch_ref);
4153        }
4154
4155        sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
4156    }
4157
4158    async fn add_arc_size_constraint(
4159        &mut self,
4160        sketch: ObjectId,
4161        params: ArcSizeConstraintParams,
4162        new_ast: &mut ast::Node<ast::Program>,
4163    ) -> Result<AstNodeRef, KclError> {
4164        let sketch_id = sketch;
4165
4166        // Constraint must have exactly 1 argument (arc segment)
4167        if params.points.len() != 1 {
4168            return Err(KclError::refactor(format!(
4169                "{} constraint must have exactly 1 argument (an arc segment), got {}",
4170                params.constraint_type_name,
4171                params.points.len()
4172            )));
4173        }
4174
4175        let arc_id = params.points[0];
4176        let arc_object = self
4177            .scene_graph
4178            .objects
4179            .get(arc_id.0)
4180            .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
4181        let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
4182            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
4183        };
4184        let ref_type = match arc_segment {
4185            Segment::Arc(_) => ARC_VARIABLE,
4186            Segment::Circle(_) => CIRCLE_VARIABLE,
4187            _ => {
4188                return Err(KclError::refactor(format!(
4189                    "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
4190                    params.constraint_type_name
4191                )));
4192            }
4193        };
4194        // Reference the arc/circle segment directly
4195        let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
4196        let arguments = match &params.label_position {
4197            Some(label_position) => vec![ast::LabeledArg {
4198                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4199                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4200            }],
4201            None => Default::default(),
4202        };
4203
4204        // Create the function call.
4205        let call_ast = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4206            callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
4207            unlabeled: Some(arc_ast),
4208            arguments,
4209            digest: None,
4210            non_code_meta: Default::default(),
4211        })));
4212        let constraint_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
4213            left: call_ast,
4214            operator: ast::BinaryOperator::Eq,
4215            right: ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
4216                value: ast::LiteralValue::Number {
4217                    value: params.value,
4218                    suffix: params.units,
4219                },
4220                raw: format_number_literal(params.value, params.units, None)
4221                    .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
4222                digest: None,
4223            }))),
4224            digest: None,
4225        })));
4226
4227        // Add the line to the AST of the sketch block.
4228        let (sketch_block_ref, _) = self.mutate_ast(
4229            new_ast,
4230            sketch_id,
4231            AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
4232        )?;
4233        Ok(sketch_block_ref)
4234    }
4235
4236    async fn add_horizontal_distance(
4237        &mut self,
4238        sketch: ObjectId,
4239        distance: Distance,
4240        new_ast: &mut ast::Node<ast::Program>,
4241    ) -> Result<AstNodeRef, KclError> {
4242        self.add_distance_constraint(sketch, HORIZONTAL_DISTANCE_FN, distance, new_ast)
4243    }
4244
4245    async fn add_vertical_distance(
4246        &mut self,
4247        sketch: ObjectId,
4248        distance: Distance,
4249        new_ast: &mut ast::Node<ast::Program>,
4250    ) -> Result<AstNodeRef, KclError> {
4251        self.add_distance_constraint(sketch, VERTICAL_DISTANCE_FN, distance, new_ast)
4252    }
4253
4254    async fn add_horizontal(
4255        &mut self,
4256        sketch: ObjectId,
4257        horizontal: Horizontal,
4258        new_ast: &mut ast::Node<ast::Program>,
4259    ) -> Result<AstNodeRef, KclError> {
4260        let sketch_id = sketch;
4261
4262        // Map the runtime objects back to variable names.
4263        let first_arg_ast = match horizontal {
4264            Horizontal::Line { line } => {
4265                let line_object = self
4266                    .scene_graph
4267                    .objects
4268                    .get(line.0)
4269                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4270                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4271                    let kind = line_object.kind.human_friendly_kind_with_article();
4272                    return Err(KclError::refactor(format!(
4273                        "This constraint only works on Segments, but you selected {kind}"
4274                    )));
4275                };
4276                let Segment::Line(_) = line_segment else {
4277                    return Err(KclError::refactor(format!(
4278                        "Only lines can be made horizontal, but you selected {}",
4279                        line_segment.human_friendly_kind_with_article(),
4280                    )));
4281                };
4282                self.line_id_to_ast_reference(line, new_ast)?
4283            }
4284            Horizontal::Points { points } => {
4285                let point_asts = points
4286                    .iter()
4287                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4288                    .collect::<Result<Vec<_>, _>>()?;
4289                ast::ArrayExpression::new(point_asts).into()
4290            }
4291        };
4292        // Create the horizontal() call using shared helper.
4293        let horizontal_ast = create_horizontal_ast(first_arg_ast);
4294
4295        // Add the line to the AST of the sketch block.
4296        let (sketch_block_ref, _) = self.mutate_ast(
4297            new_ast,
4298            sketch_id,
4299            AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
4300        )?;
4301        Ok(sketch_block_ref)
4302    }
4303
4304    async fn add_lines_equal_length(
4305        &mut self,
4306        sketch: ObjectId,
4307        lines_equal_length: LinesEqualLength,
4308        new_ast: &mut ast::Node<ast::Program>,
4309    ) -> Result<AstNodeRef, KclError> {
4310        if lines_equal_length.lines.len() < 2 {
4311            return Err(KclError::refactor(format!(
4312                "Lines equal length constraint must have at least 2 lines, got {}",
4313                lines_equal_length.lines.len()
4314            )));
4315        };
4316
4317        let sketch_id = sketch;
4318
4319        // Map the runtime objects back to variable names.
4320        let line_asts = lines_equal_length
4321            .lines
4322            .iter()
4323            .map(|line_id| {
4324                let line_object = self
4325                    .scene_graph
4326                    .objects
4327                    .get(line_id.0)
4328                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4329                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4330                    let kind = line_object.kind.human_friendly_kind_with_article();
4331                    return Err(KclError::refactor(format!(
4332                        "This constraint only works on Segments, but you selected {kind}"
4333                    )));
4334                };
4335                let Segment::Line(_) = line_segment else {
4336                    let kind = line_segment.human_friendly_kind_with_article();
4337                    return Err(KclError::refactor(format!(
4338                        "Only lines can be made equal length, but you selected {kind}"
4339                    )));
4340                };
4341
4342                self.line_id_to_ast_reference(*line_id, new_ast)
4343            })
4344            .collect::<Result<Vec<_>, _>>()?;
4345
4346        // Create the equalLength() call using shared helper.
4347        let equal_length_ast = create_equal_length_ast(line_asts);
4348
4349        // Add the constraint to the AST of the sketch block.
4350        let (sketch_block_ref, _) = self.mutate_ast(
4351            new_ast,
4352            sketch_id,
4353            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
4354        )?;
4355        Ok(sketch_block_ref)
4356    }
4357
4358    fn equal_radius_segment_id_to_ast_reference(
4359        &mut self,
4360        segment_id: ObjectId,
4361        new_ast: &mut ast::Node<ast::Program>,
4362    ) -> Result<ast::Expr, KclError> {
4363        let segment_object = self
4364            .scene_graph
4365            .objects
4366            .get(segment_id.0)
4367            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4368        let ObjectKind::Segment { segment } = &segment_object.kind else {
4369            return Err(KclError::refactor(format!(
4370                "Object is not a segment, it was {}",
4371                segment_object.kind.human_friendly_kind_with_article()
4372            )));
4373        };
4374
4375        let ref_type = match segment {
4376            Segment::Arc(_) => ARC_VARIABLE,
4377            Segment::Circle(_) => CIRCLE_VARIABLE,
4378            _ => {
4379                return Err(KclError::refactor(format!(
4380                    "equalRadius supports only arc/circle segments, got {}",
4381                    segment.human_friendly_kind_with_article()
4382                )));
4383            }
4384        };
4385
4386        get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
4387    }
4388
4389    fn symmetric_input_id_to_ast_reference(
4390        &mut self,
4391        segment_id: ObjectId,
4392        new_ast: &mut ast::Node<ast::Program>,
4393    ) -> Result<ast::Expr, KclError> {
4394        let segment_object = self
4395            .scene_graph
4396            .objects
4397            .get(segment_id.0)
4398            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4399        let ObjectKind::Segment { segment } = &segment_object.kind else {
4400            return Err(KclError::refactor(format!(
4401                "Object is not a segment, it was {}",
4402                segment_object.kind.human_friendly_kind_with_article()
4403            )));
4404        };
4405
4406        match segment {
4407            Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
4408            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
4409            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
4410            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
4411            Segment::ControlPointSpline(_) => Err(KclError::refactor(
4412                "Symmetric does not yet support control point splines".to_owned(),
4413            )),
4414        }
4415    }
4416
4417    fn symmetric_axis_id_to_ast_reference(
4418        &mut self,
4419        segment_id: ObjectId,
4420        new_ast: &mut ast::Node<ast::Program>,
4421    ) -> Result<ast::Expr, KclError> {
4422        let segment_object = self
4423            .scene_graph
4424            .objects
4425            .get(segment_id.0)
4426            .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
4427        let ObjectKind::Segment { segment } = &segment_object.kind else {
4428            return Err(KclError::refactor(format!(
4429                "Object is not a segment, it was {}",
4430                segment_object.kind.human_friendly_kind_with_article()
4431            )));
4432        };
4433        match segment {
4434            Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
4435            _ => Err(KclError::refactor(format!(
4436                "Symmetric axis must be a line, got {}",
4437                segment.human_friendly_kind_with_article()
4438            ))),
4439        }
4440    }
4441
4442    async fn add_parallel(
4443        &mut self,
4444        sketch: ObjectId,
4445        parallel: Parallel,
4446        new_ast: &mut ast::Node<ast::Program>,
4447    ) -> Result<AstNodeRef, KclError> {
4448        if parallel.lines.len() < 2 {
4449            return Err(KclError::refactor(format!(
4450                "Parallel constraint must have at least 2 lines, got {}",
4451                parallel.lines.len()
4452            )));
4453        };
4454
4455        let sketch_id = sketch;
4456
4457        let line_asts = parallel
4458            .lines
4459            .iter()
4460            .map(|line_id| {
4461                let line_object = self
4462                    .scene_graph
4463                    .objects
4464                    .get(line_id.0)
4465                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4466                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4467                    let kind = line_object.kind.human_friendly_kind_with_article();
4468                    return Err(KclError::refactor(format!(
4469                        "This constraint only works on Segments, but you selected {kind}"
4470                    )));
4471                };
4472                let Segment::Line(_) = line_segment else {
4473                    let kind = line_segment.human_friendly_kind_with_article();
4474                    return Err(KclError::refactor(format!(
4475                        "Only lines can be made parallel, but you selected {kind}"
4476                    )));
4477                };
4478
4479                self.line_id_to_ast_reference(*line_id, new_ast)
4480            })
4481            .collect::<Result<Vec<_>, _>>()?;
4482
4483        let call_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4484            callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
4485            unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
4486                ast::ArrayExpression {
4487                    elements: line_asts,
4488                    digest: None,
4489                    non_code_meta: Default::default(),
4490                },
4491            )))),
4492            arguments: Default::default(),
4493            digest: None,
4494            non_code_meta: Default::default(),
4495        })));
4496
4497        let (sketch_block_ref, _) = self.mutate_ast(
4498            new_ast,
4499            sketch_id,
4500            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4501        )?;
4502        Ok(sketch_block_ref)
4503    }
4504
4505    async fn add_perpendicular(
4506        &mut self,
4507        sketch: ObjectId,
4508        perpendicular: Perpendicular,
4509        new_ast: &mut ast::Node<ast::Program>,
4510    ) -> Result<AstNodeRef, KclError> {
4511        self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
4512            .await
4513    }
4514
4515    async fn add_lines_at_angle_constraint(
4516        &mut self,
4517        sketch: ObjectId,
4518        angle_kind: LinesAtAngleKind,
4519        lines: Vec<ObjectId>,
4520        new_ast: &mut ast::Node<ast::Program>,
4521    ) -> Result<AstNodeRef, KclError> {
4522        let &[line0_id, line1_id] = lines.as_slice() else {
4523            return Err(KclError::refactor(format!(
4524                "{} constraint must have exactly 2 lines, got {}",
4525                angle_kind.to_function_name(),
4526                lines.len()
4527            )));
4528        };
4529
4530        let sketch_id = sketch;
4531
4532        // Map the runtime objects back to variable names.
4533        let line0_object = self
4534            .scene_graph
4535            .objects
4536            .get(line0_id.0)
4537            .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
4538        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
4539            let kind = line0_object.kind.human_friendly_kind_with_article();
4540            return Err(KclError::refactor(format!(
4541                "This constraint only works on Segments, but you selected {kind}"
4542            )));
4543        };
4544        let Segment::Line(_) = line0_segment else {
4545            return Err(KclError::refactor(format!(
4546                "Only lines can be made {}, but you selected {}",
4547                angle_kind.to_function_name(),
4548                line0_segment.human_friendly_kind_with_article(),
4549            )));
4550        };
4551        let line0_ast = self.line_id_to_ast_reference(line0_id, new_ast)?;
4552
4553        let line1_object = self
4554            .scene_graph
4555            .objects
4556            .get(line1_id.0)
4557            .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
4558        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
4559            let kind = line1_object.kind.human_friendly_kind_with_article();
4560            return Err(KclError::refactor(format!(
4561                "This constraint only works on Segments, but you selected {kind}"
4562            )));
4563        };
4564        let Segment::Line(_) = line1_segment else {
4565            return Err(KclError::refactor(format!(
4566                "Only lines can be made {}, but you selected {}",
4567                angle_kind.to_function_name(),
4568                line1_segment.human_friendly_kind_with_article(),
4569            )));
4570        };
4571        let line1_ast = self.line_id_to_ast_reference(line1_id, new_ast)?;
4572
4573        // Create the parallel() or perpendicular() call.
4574        let call_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4575            callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
4576            unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
4577                ast::ArrayExpression {
4578                    elements: vec![line0_ast, line1_ast],
4579                    digest: None,
4580                    non_code_meta: Default::default(),
4581                },
4582            )))),
4583            arguments: Default::default(),
4584            digest: None,
4585            non_code_meta: Default::default(),
4586        })));
4587
4588        // Add the constraint to the AST of the sketch block.
4589        let (sketch_block_ref, _) = self.mutate_ast(
4590            new_ast,
4591            sketch_id,
4592            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4593        )?;
4594        Ok(sketch_block_ref)
4595    }
4596
4597    async fn add_vertical(
4598        &mut self,
4599        sketch: ObjectId,
4600        vertical: Vertical,
4601        new_ast: &mut ast::Node<ast::Program>,
4602    ) -> Result<AstNodeRef, KclError> {
4603        let sketch_id = sketch;
4604
4605        let first_arg_ast = match vertical {
4606            Vertical::Line { line } => {
4607                // Map the runtime objects back to variable names.
4608                let line_object = self
4609                    .scene_graph
4610                    .objects
4611                    .get(line.0)
4612                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4613                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4614                    let kind = line_object.kind.human_friendly_kind_with_article();
4615                    return Err(KclError::refactor(format!(
4616                        "This constraint only works on Segments, but you selected {kind}"
4617                    )));
4618                };
4619                let Segment::Line(_) = line_segment else {
4620                    return Err(KclError::refactor(format!(
4621                        "Only lines can be made vertical, but you selected {}",
4622                        line_segment.human_friendly_kind_with_article()
4623                    )));
4624                };
4625                self.line_id_to_ast_reference(line, new_ast)?
4626            }
4627            Vertical::Points { points } => {
4628                let point_asts = points
4629                    .iter()
4630                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4631                    .collect::<Result<Vec<_>, _>>()?;
4632                ast::ArrayExpression::new(point_asts).into()
4633            }
4634        };
4635        // Create the vertical() call using shared helper.
4636        let vertical_ast = create_vertical_ast(first_arg_ast);
4637
4638        // Add the line to the AST of the sketch block.
4639        let (sketch_block_ref, _) = self.mutate_ast(
4640            new_ast,
4641            sketch_id,
4642            AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4643        )?;
4644        Ok(sketch_block_ref)
4645    }
4646
4647    async fn execute_after_add_constraint(
4648        &mut self,
4649        ctx: &ExecutorContext,
4650        sketch_id: ObjectId,
4651        sketch_block_ref: AstNodeRef,
4652        new_ast: &mut ast::Node<ast::Program>,
4653    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4654        // Convert to string source to create real source ranges.
4655        let new_source = source_from_ast(new_ast);
4656        // Parse the new KCL source.
4657        let new_program = parse_frontend_mutation_source(
4658            &new_source,
4659            "Error parsing KCL source after adding constraint",
4660            "No AST produced after adding constraint",
4661        )?;
4662        let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4663            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4664                "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4665            )))
4666        })?;
4667
4668        // Truncate after the sketch block for mock execution.
4669        // Use a clone so we don't mutate new_program yet
4670        let mut truncated_program = new_program.clone();
4671        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4672            .map_err(KclErrorWithOutputs::no_outputs)?;
4673
4674        // Execute - if this fails, we haven't modified self yet, so state is safe
4675        let outcome = ctx
4676            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4677            .await?;
4678
4679        let new_object_ids = {
4680            // Extract the constraint ID from the execution outcome using source_range_to_object
4681            let constraint_id = outcome
4682                .source_range_to_object
4683                .get(&constraint_node_ref.range)
4684                .copied()
4685                .ok_or_else(|| {
4686                    KclErrorWithOutputs::from_error_outcome(
4687                        KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4688                        outcome.clone(),
4689                    )
4690                })?;
4691            vec![constraint_id]
4692        };
4693
4694        // Only now, after all operations succeeded, update self.program.
4695        // This ensures state is only modified if everything succeeds.
4696        self.program = new_program;
4697
4698        // Uses MockConfig::default() which has freedom_analysis: true
4699        let outcome = self.update_state_after_exec(outcome, true);
4700
4701        let src_delta = self.commit_var_solutions_to_program(&outcome, "adding constraint")?;
4702        let scene_graph_delta = SceneGraphDelta {
4703            new_graph: self.scene_graph_for_ui(),
4704            invalidates_ids: false,
4705            new_objects: new_object_ids,
4706            exec_outcome: outcome,
4707        };
4708        Ok((src_delta, scene_graph_delta))
4709    }
4710
4711    fn commit_var_solutions_to_program(&mut self, outcome: &ExecOutcome, operation: &str) -> ExecResult<SourceDelta> {
4712        let commit_failure = || {
4713            KclErrorWithOutputs::from_error_outcome(
4714                KclError::refactor(format!("Could not update KCL after {operation}.")),
4715                outcome.clone(),
4716            )
4717        };
4718
4719        let default_length_unit = self.default_length_unit();
4720        let mut settled_ast = self.program.ast.clone();
4721        let mut committed_solver_value = false;
4722        for (var_range, node_path, value) in &outcome.var_solutions {
4723            let Some(lookup) = numeric_literal_at_node_path(&settled_ast, node_path.as_ref(), *var_range) else {
4724                return Err(commit_failure());
4725            };
4726            let new_value = match &lookup {
4727                Some(current_literal) => {
4728                    if !var_solution_needs_commit(current_literal, *value, default_length_unit) {
4729                        continue;
4730                    }
4731                    preserve_var_solution_literal_style(current_literal, *value, default_length_unit)
4732                }
4733                None => {
4734                    // Bare `var` with no initial literal to compare against;
4735                    // always commit, using the module's default length unit as
4736                    // an explicit suffix so the written value carries units.
4737                    Number {
4738                        value: number_value_in_default_length_units(*value, default_length_unit),
4739                        units: default_length_unit.into(),
4740                    }
4741                }
4742            };
4743            committed_solver_value = true;
4744            let source_ref = SourceRef::Simple {
4745                range: *var_range,
4746                node_path: node_path.clone(),
4747            };
4748            mutate_ast_node_by_source_ref(
4749                &mut settled_ast,
4750                &source_ref,
4751                AstMutateCommand::EditVarInitialValue { value: new_value },
4752            )
4753            .map_err(|_| commit_failure())?;
4754        }
4755
4756        if !committed_solver_value {
4757            return Ok(SourceDelta {
4758                text: self.program.original_file_contents.clone(),
4759            });
4760        }
4761
4762        let settled_source = source_from_ast(&settled_ast);
4763        let (settled_program, errors) = Program::parse(&settled_source).map_err(|_| commit_failure())?;
4764        if !errors.is_empty() {
4765            return Err(commit_failure());
4766        }
4767        let Some(settled_program) = settled_program else {
4768            return Err(commit_failure());
4769        };
4770
4771        self.program = settled_program;
4772
4773        Ok(SourceDelta { text: settled_source })
4774    }
4775
4776    // Find constraints that reference the given segments.
4777    fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4778        if segment_ids_set.contains(&segment_id) {
4779            return true;
4780        }
4781
4782        let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4783            return false;
4784        };
4785        let ObjectKind::Segment { segment } = &segment_object.kind else {
4786            return false;
4787        };
4788        let Segment::Point(point) = segment else {
4789            return false;
4790        };
4791
4792        point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4793    }
4794
4795    fn remaining_constraint_segments(
4796        &self,
4797        segments: &[ConstraintSegment],
4798        segment_ids_set: &AhashIndexSet<ObjectId>,
4799    ) -> Vec<ConstraintSegment> {
4800        segments
4801            .iter()
4802            .copied()
4803            .filter(|segment| match segment {
4804                ConstraintSegment::Origin(_) => true,
4805                ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4806            })
4807            .collect()
4808    }
4809
4810    fn find_referenced_constraints(
4811        &self,
4812        sketch_id: ObjectId,
4813        segment_ids_set: &AhashIndexSet<ObjectId>,
4814    ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4815        // Look up the sketch.
4816        let sketch_object = self
4817            .scene_graph
4818            .objects
4819            .get(sketch_id.0)
4820            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4821        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4822            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4823        };
4824        let segment_or_owner_matches = |segment_id: ObjectId| {
4825            if segment_ids_set.contains(&segment_id) {
4826                return true;
4827            }
4828            let segment_object = self.scene_graph.objects.get(segment_id.0);
4829            if let Some(obj) = segment_object
4830                && let ObjectKind::Segment { segment } = &obj.kind
4831            {
4832                match segment {
4833                    Segment::Point(point) => point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4834                    Segment::Line(line) => line.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4835                    _ => false,
4836                }
4837            } else {
4838                false
4839            }
4840        };
4841        let mut constraint_ids_set = AhashIndexSet::default();
4842        for constraint_id in &sketch.constraints {
4843            let constraint_object = self
4844                .scene_graph
4845                .objects
4846                .get(constraint_id.0)
4847                .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4848            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4849                return Err(KclError::refactor(format!(
4850                    "Object is not a constraint, it is {}",
4851                    constraint_object.kind.human_friendly_kind_with_article()
4852                )));
4853            };
4854            let depends_on_segment = match constraint {
4855                Constraint::Coincident(c) => c.segment_ids().any(segment_or_owner_matches),
4856                Constraint::Distance(d) => d.segment_ids().any(segment_or_owner_matches),
4857                Constraint::Fixed(fixed) => fixed
4858                    .points
4859                    .iter()
4860                    .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4861                Constraint::Radius(r) => segment_or_owner_matches(r.arc),
4862                Constraint::Diameter(d) => segment_or_owner_matches(d.arc),
4863                Constraint::EqualRadius(equal_radius) => {
4864                    equal_radius.input.iter().copied().any(segment_or_owner_matches)
4865                }
4866                Constraint::HorizontalDistance(d) => d.segment_ids().any(segment_or_owner_matches),
4867                Constraint::VerticalDistance(d) => d.segment_ids().any(segment_or_owner_matches),
4868                Constraint::Horizontal(h) => match h {
4869                    Horizontal::Line { line } => segment_or_owner_matches(*line),
4870                    Horizontal::Points { points } => points.iter().any(|point| match point {
4871                        ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4872                        ConstraintSegment::Origin(_) => false,
4873                    }),
4874                },
4875                Constraint::Vertical(v) => match v {
4876                    Vertical::Line { line } => segment_or_owner_matches(*line),
4877                    Vertical::Points { points } => points.iter().any(|point| match point {
4878                        ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4879                        ConstraintSegment::Origin(_) => false,
4880                    }),
4881                },
4882                Constraint::LinesEqualLength(lines_equal_length) => {
4883                    lines_equal_length.lines.iter().copied().any(segment_or_owner_matches)
4884                }
4885                Constraint::Midpoint(midpoint) => {
4886                    segment_or_owner_matches(midpoint.segment)
4887                        || matches!(
4888                            midpoint.point,
4889                            ConstraintSegment::Segment(point) if segment_or_owner_matches(point)
4890                        )
4891                }
4892                Constraint::Parallel(parallel) => parallel.lines.iter().copied().any(segment_or_owner_matches),
4893                Constraint::Perpendicular(perpendicular) => {
4894                    perpendicular.lines.iter().copied().any(segment_or_owner_matches)
4895                }
4896                Constraint::Angle(angle) => angle.lines.iter().copied().any(segment_or_owner_matches),
4897                Constraint::Symmetric(symmetric) => {
4898                    segment_or_owner_matches(symmetric.axis)
4899                        || symmetric.input.iter().copied().any(segment_or_owner_matches)
4900                }
4901                Constraint::Tangent(tangent) => tangent.input.iter().copied().any(segment_or_owner_matches),
4902            };
4903            if depends_on_segment {
4904                constraint_ids_set.insert(*constraint_id);
4905            }
4906        }
4907        Ok(constraint_ids_set)
4908    }
4909
4910    fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4911        let mut outcome = outcome;
4912        self.solid_references = solid_references_from_variables(&self.program.ast, &outcome.variables);
4913        let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4914
4915        if freedom_analysis_ran {
4916            // When freedom analysis ran, replace the cache entirely with new values
4917            // Don't merge with old values since IDs might have changed
4918            self.point_freedom_cache.clear();
4919            for new_obj in &new_objects {
4920                if let ObjectKind::Segment {
4921                    segment: crate::front::Segment::Point(point),
4922                } = &new_obj.kind
4923                {
4924                    self.point_freedom_cache.insert(new_obj.id, point.freedom);
4925                }
4926            }
4927            add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4928            // Objects are already correct from the analysis, just use them as-is
4929            self.scene_graph.objects = new_objects;
4930        } else {
4931            // When freedom analysis didn't run, preserve old values and merge
4932            // Before replacing objects, extract and store freedom values from old objects
4933            for old_obj in &self.scene_graph.objects {
4934                if let ObjectKind::Segment {
4935                    segment: crate::front::Segment::Point(point),
4936                } = &old_obj.kind
4937                {
4938                    self.point_freedom_cache.insert(old_obj.id, point.freedom);
4939                }
4940            }
4941
4942            // Update objects, preserving stored freedom values when new is Free (might be default)
4943            let mut updated_objects = Vec::with_capacity(new_objects.len());
4944            for new_obj in new_objects {
4945                let mut obj = new_obj;
4946                if let ObjectKind::Segment {
4947                    segment: crate::front::Segment::Point(point),
4948                } = &mut obj.kind
4949                {
4950                    let new_freedom = point.freedom;
4951                    // When freedom_analysis=false, new values are defaults (Free).
4952                    // Only preserve cached values when new is Free (indicating it's a default, not from analysis).
4953                    // If new is NOT Free, use the new value (it came from somewhere else, maybe conflict detection).
4954                    // Never preserve Conflict from cache - conflicts are transient and should only be set
4955                    // when there are actually unsatisfied constraints.
4956                    match new_freedom {
4957                        Freedom::Free => {
4958                            match self.point_freedom_cache.get(&obj.id).copied() {
4959                                Some(Freedom::Conflict) => {
4960                                    // Don't preserve Conflict - conflicts are transient
4961                                    // Keep it as Free
4962                                }
4963                                Some(Freedom::Fixed) => {
4964                                    // Preserve Fixed cached value
4965                                    point.freedom = Freedom::Fixed;
4966                                }
4967                                Some(Freedom::Free) => {
4968                                    // If stored is also Free, keep Free (no change needed)
4969                                }
4970                                None => {
4971                                    // If no cached value, keep Free (default)
4972                                }
4973                            }
4974                        }
4975                        Freedom::Fixed => {
4976                            // Use new value (already set)
4977                        }
4978                        Freedom::Conflict => {
4979                            // Use new value (already set)
4980                        }
4981                    }
4982                    // Store the new freedom value (even if it's Free, so we know it was set)
4983                    self.point_freedom_cache.insert(obj.id, point.freedom);
4984                }
4985                updated_objects.push(obj);
4986            }
4987
4988            add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4989            self.scene_graph.objects = updated_objects;
4990        }
4991        outcome
4992    }
4993
4994    fn mutate_ast(
4995        &mut self,
4996        ast: &mut ast::Node<ast::Program>,
4997        object_id: ObjectId,
4998        command: AstMutateCommand,
4999    ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5000        let sketch_object = self
5001            .scene_graph
5002            .objects
5003            .get(object_id.0)
5004            .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
5005        mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
5006    }
5007
5008    fn mutate_constraint_label_position(
5009        &mut self,
5010        ast: &mut ast::Node<ast::Program>,
5011        constraint_id: ObjectId,
5012        label_position: Point2d<Number>,
5013    ) -> Result<(), KclError> {
5014        let object = self
5015            .scene_graph
5016            .objects
5017            .get(constraint_id.0)
5018            .ok_or_else(|| KclError::refactor(format!("Object not found: {constraint_id:?}")))?;
5019        if !matches!(
5020            &object.kind,
5021            ObjectKind::Constraint {
5022                constraint: Constraint::Distance(_)
5023                    | Constraint::HorizontalDistance(_)
5024                    | Constraint::VerticalDistance(_)
5025                    | Constraint::Radius(_)
5026                    | Constraint::Diameter(_)
5027                    | Constraint::Angle(_),
5028            }
5029        ) {
5030            return Err(KclError::refactor(format!(
5031                "Object does not support labelPosition: {constraint_id:?}"
5032            )));
5033        }
5034
5035        let label_position = to_ast_point2d_number(&label_position)
5036            .map_err(|err| KclError::refactor(format!("Could not convert label position to AST: {err}")))?;
5037        self.mutate_ast(
5038            ast,
5039            constraint_id,
5040            AstMutateCommand::EditDistanceConstraintLabelPosition { label_position },
5041        )?;
5042        Ok(())
5043    }
5044}
5045
5046fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
5047    // Look up existing sketch.
5048    let sketch_object = scene_graph
5049        .objects
5050        .get(sketch_id.0)
5051        .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
5052    let ObjectKind::Sketch(_) = &sketch_object.kind else {
5053        return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
5054    };
5055    expect_single_node_ref(sketch_object)
5056}
5057
5058fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
5059    match &object.source {
5060        SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
5061            range: *range,
5062            node_path: node_path.clone(),
5063        }),
5064        SourceRef::BackTrace { ranges } => {
5065            let [range] = ranges.as_slice() else {
5066                return Err(KclError::refactor(format!(
5067                    "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
5068                    ranges.len()
5069                )));
5070            };
5071            Ok(AstNodeRef {
5072                range: range.0,
5073                node_path: range.1.clone(),
5074            })
5075        }
5076    }
5077}
5078
5079/// This is a deprecated fall-back implementation. Prefer
5080/// [`only_sketch_block()`] to avoid reliance on source ranges.
5081fn only_sketch_block_from_range(
5082    ast: &mut ast::Node<ast::Program>,
5083    sketch_block_range: SourceRange,
5084    edit_kind: ChangeKind,
5085) -> Result<(), KclError> {
5086    let r1 = sketch_block_range;
5087    let matches_range = |r2: SourceRange| -> bool {
5088        // We may have added items to the sketch block, so the end may not be an
5089        // exact match.
5090        match edit_kind {
5091            ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
5092            // For edit, we don't know whether it grew or shrank.
5093            ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
5094            ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
5095            // No edit should be an exact match.
5096            ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
5097        }
5098    };
5099    let mut found = false;
5100    for item in ast.body.iter_mut() {
5101        match item {
5102            ast::BodyItem::ImportStatement(_) => {}
5103            ast::BodyItem::ExpressionStatement(node) => {
5104                if matches_range(SourceRange::from(&*node))
5105                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5106                {
5107                    sketch_block.is_being_edited = true;
5108                    found = true;
5109                    break;
5110                }
5111            }
5112            ast::BodyItem::VariableDeclaration(node) => {
5113                if matches_range(SourceRange::from(&node.declaration.init))
5114                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5115                {
5116                    sketch_block.is_being_edited = true;
5117                    found = true;
5118                    break;
5119                }
5120            }
5121            ast::BodyItem::TypeDeclaration(_) => {}
5122            ast::BodyItem::ReturnStatement(node) => {
5123                if matches_range(SourceRange::from(&node.argument))
5124                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5125                {
5126                    sketch_block.is_being_edited = true;
5127                    found = true;
5128                    break;
5129                }
5130            }
5131        }
5132    }
5133    if !found {
5134        return Err(KclError::refactor(format!(
5135            "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
5136        )));
5137    }
5138
5139    Ok(())
5140}
5141
5142fn only_sketch_block(
5143    ast: &mut ast::Node<ast::Program>,
5144    sketch_block_ref: &AstNodeRef,
5145    edit_kind: ChangeKind,
5146) -> Result<(), KclError> {
5147    let Some(target_node_path) = &sketch_block_ref.node_path else {
5148        #[cfg(target_arch = "wasm32")]
5149        web_sys::console::warn_1(
5150            &format!(
5151                "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
5152                sketch_block_ref
5153            )
5154            .into(),
5155        );
5156        return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
5157    };
5158    struct MarkSketchBlockBeingEdited<'a> {
5159        target_node_path: &'a ast::NodePath,
5160    }
5161
5162    impl Visitor for MarkSketchBlockBeingEdited<'_> {
5163        type Break = ();
5164        type Continue = ();
5165
5166        fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5167            if let NodeMut::SketchBlock(sketch_block) = node
5168                && sketch_block.node_path.as_ref() == Some(self.target_node_path)
5169            {
5170                sketch_block.is_being_edited = true;
5171                return TraversalReturn::new_break(());
5172            }
5173            TraversalReturn::new_continue(())
5174        }
5175
5176        fn finish(&mut self, _node: NodeMut<'_>) {}
5177    }
5178
5179    let mut marker = MarkSketchBlockBeingEdited { target_node_path };
5180    let found = dfs_mut(ast, &mut marker).is_break();
5181    if !found {
5182        return Err(KclError::refactor(format!(
5183            "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
5184        )));
5185    }
5186
5187    Ok(())
5188}
5189
5190fn sketch_on_ast_expr(
5191    ast: &mut ast::Node<ast::Program>,
5192    scene_graph: &SceneGraph,
5193    solid_references: &HashMap<Uuid, SolidAstReference>,
5194    on: &Plane,
5195) -> Result<ast::Expr, KclError> {
5196    match on {
5197        Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
5198        Plane::Object(object_id) => {
5199            let on_object = scene_graph
5200                .objects
5201                .get(object_id.0)
5202                .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
5203            if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
5204                return Ok(face_expr);
5205            }
5206            get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
5207        }
5208        Plane::PrimitiveFace(face) => {
5209            let solid_expr = solid_expr_for_engine_id(solid_references, face.solid_id).ok_or_else(|| {
5210                KclError::refactor(format!(
5211                    "Could not resolve a KCL solid for selected primitive face: solid_id={}",
5212                    face.solid_id
5213                ))
5214            })?;
5215            let face_id_expr = create_face_id_ast(solid_expr.clone(), face.index);
5216            Ok(create_face_of_ast(solid_expr, face_id_expr))
5217        }
5218    }
5219}
5220
5221fn solid_references_from_variables(
5222    ast: &ast::Node<ast::Program>,
5223    variables: &IndexMap<String, KclValueView>,
5224) -> HashMap<Uuid, SolidAstReference> {
5225    let mut references = HashMap::new();
5226
5227    // In-place operations such as shell reuse their input solid's engine ID.
5228    // Later variables overwrite earlier references so mutations target the result.
5229    for item in &ast.body {
5230        let ast::BodyItem::VariableDeclaration(declaration) = item else {
5231            continue;
5232        };
5233        let name = &declaration.declaration.id.name;
5234        let Some(value) = variables.get(name) else {
5235            continue;
5236        };
5237
5238        match value {
5239            KclValueView::Solid { value } => {
5240                references.insert(
5241                    value.id,
5242                    SolidAstReference {
5243                        variable_name: name.clone(),
5244                        output_index: None,
5245                    },
5246                );
5247            }
5248            KclValueView::Tuple { value } | KclValueView::HomArray { value } => {
5249                for (output_index, entry) in value.iter().enumerate() {
5250                    if let KclValueView::Solid { value } = entry {
5251                        references.insert(
5252                            value.id,
5253                            SolidAstReference {
5254                                variable_name: name.clone(),
5255                                output_index: Some(output_index),
5256                            },
5257                        );
5258                    }
5259                }
5260            }
5261            _ => {}
5262        }
5263    }
5264
5265    references
5266}
5267
5268fn solid_expr_for_engine_id(solid_references: &HashMap<Uuid, SolidAstReference>, solid_id: Uuid) -> Option<ast::Expr> {
5269    let reference = solid_references.get(&solid_id)?;
5270    let solid_expr = ast_name_expr(reference.variable_name.clone());
5271    Some(indexed_solid_expr_for_sweep_output(solid_expr, reference.output_index))
5272}
5273
5274fn sketch_face_of_scene_object_ast_expr(
5275    ast: &mut ast::Node<ast::Program>,
5276    on_object: &crate::front::Object,
5277) -> Result<Option<ast::Expr>, KclError> {
5278    match &on_object.kind {
5279        ObjectKind::Wall(wall) => {
5280            let solid_ref = get_or_insert_ast_reference(
5281                ast,
5282                &source_ref_from_source_ref_range(&wall.source.solid),
5283                "solid",
5284                None,
5285            )?;
5286            let ast::Expr::Name(solid_name_expr) = solid_ref else {
5287                return Err(KclError::refactor(format!(
5288                    "Could not resolve solid reference for selected wall: artifact_id={:?}",
5289                    on_object.artifact_id
5290                )));
5291            };
5292            let solid_expr = indexed_solid_expr_for_sweep_output(
5293                ast_name_expr(solid_name_expr.name.name.clone()),
5294                wall.solid_output_index,
5295            );
5296            let sweep_ref = get_or_insert_ast_reference(
5297                ast,
5298                &source_ref_from_source_ref_range(&wall.source.sweep),
5299                "solid",
5300                None,
5301            )?;
5302            let ast::Expr::Name(sweep_name_expr) = sweep_ref else {
5303                return Err(KclError::refactor(format!(
5304                    "Could not resolve sweep reference for selected wall: artifact_id={:?}",
5305                    on_object.artifact_id
5306                )));
5307            };
5308            let sweep_name = sweep_name_expr.name.name.clone();
5309            let segment_ref = get_or_insert_ast_reference(
5310                ast,
5311                &source_ref_from_source_ref_range(&wall.source.segment),
5312                LINE_VARIABLE,
5313                None,
5314            )?;
5315
5316            let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &sweep_name).or_else(|| {
5317                wall.source
5318                    .path
5319                    .as_ref()
5320                    .and_then(|path_source| region_name_from_path_source(ast, path_source))
5321            }) {
5322                let ast::Expr::Name(segment_name_expr) = segment_ref else {
5323                    return Err(KclError::refactor(format!(
5324                        "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
5325                        on_object.artifact_id
5326                    )));
5327                };
5328                create_member_expression(
5329                    create_member_expression(ast_name_expr(region_name), "tags"),
5330                    &segment_name_expr.name.name,
5331                )
5332            } else {
5333                segment_ref
5334            };
5335
5336            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5337        }
5338        ObjectKind::Cap(cap) => {
5339            let solid_ref =
5340                get_or_insert_ast_reference(ast, &source_ref_from_source_ref_range(&cap.source.solid), "solid", None)?;
5341            let ast::Expr::Name(solid_name_expr) = solid_ref else {
5342                return Err(KclError::refactor(format!(
5343                    "Could not resolve solid reference for selected cap: artifact_id={:?}",
5344                    on_object.artifact_id
5345                )));
5346            };
5347            let solid_expr = indexed_solid_expr_for_sweep_output(
5348                ast_name_expr(solid_name_expr.name.name.clone()),
5349                cap.solid_output_index,
5350            );
5351            // TODO: change this to explicit tag references with tagStart/tagEnd mutations
5352            let face_expr = match cap.kind {
5353                crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
5354                crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
5355            };
5356
5357            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5358        }
5359        _ => Ok(None),
5360    }
5361}
5362
5363fn indexed_solid_expr_for_sweep_output(solid_expr: ast::Expr, solid_output_index: Option<usize>) -> ast::Expr {
5364    match solid_output_index {
5365        Some(output_index) => create_index_expression(solid_expr, output_index),
5366        None => solid_expr,
5367    }
5368}
5369
5370fn source_ref_from_source_ref_range(source: &SourceRefRange) -> SourceRef {
5371    SourceRef::Simple {
5372        range: source.range,
5373        node_path: source.node_path.clone(),
5374    }
5375}
5376
5377fn region_name_from_path_source(ast: &ast::Node<ast::Program>, path_source: &SourceRefRange) -> Option<String> {
5378    let source_ref = source_ref_from_source_ref_range(path_source);
5379    let candidate = variable_name_containing_source_ref(ast, &source_ref)?;
5380    let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5381        return None;
5382    };
5383    let ast::Expr::CallExpressionKw(region_call) = &region_decl.init else {
5384        return None;
5385    };
5386    if region_call.callee.name.name != "region" {
5387        return None;
5388    }
5389    Some(candidate)
5390}
5391
5392fn downstream_composite_code_ref_for_source(artifact_graph: &ArtifactGraph, source_id: ArtifactId) -> Option<&CodeRef> {
5393    let mut current_id = source_id;
5394    let mut current_composite = None;
5395    let mut visited = HashSet::new();
5396
5397    while visited.insert(current_id) {
5398        let next_composite_id = downstream_composite_id_for_solid_source(artifact_graph, current_id);
5399
5400        let Some(composite_id) = next_composite_id else {
5401            break;
5402        };
5403        let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id) else {
5404            break;
5405        };
5406
5407        current_id = composite.id;
5408        current_composite = Some(composite);
5409
5410        if !composite.consumed {
5411            break;
5412        }
5413    }
5414
5415    current_composite.map(|composite| &composite.code_ref)
5416}
5417
5418fn downstream_composite_id_for_solid_source(
5419    artifact_graph: &ArtifactGraph,
5420    source_id: ArtifactId,
5421) -> Option<ArtifactId> {
5422    // Source is a path, find its solid.
5423    if let Some(Artifact::Path(path)) = artifact_graph.get(&source_id)
5424        && let Some(composite_id) = path.composite_solid_id
5425        && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5426        && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5427    {
5428        return Some(composite_id);
5429    }
5430
5431    // Source is a sweep, find its path -> then find the solid
5432    for artifact in artifact_graph.values() {
5433        if let Artifact::Path(path) = artifact
5434            && path.sweep_id == Some(source_id)
5435            && let Some(composite_id) = path.composite_solid_id
5436            && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5437            && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5438        {
5439            return Some(composite_id);
5440        }
5441    }
5442
5443    // Source is a solid, find its downstream solid.
5444    artifact_graph.values().find_map(|artifact| {
5445        let Artifact::CompositeSolid(composite) = artifact else {
5446            return None;
5447        };
5448        composite_contains_input(&composite.solid_ids, &composite.tool_ids, source_id).then_some(composite.id)
5449    })
5450}
5451
5452fn composite_contains_path_input(
5453    solid_ids: &[ArtifactId],
5454    tool_ids: &[ArtifactId],
5455    path_id: ArtifactId,
5456    solid2d_id: Option<ArtifactId>,
5457) -> bool {
5458    composite_contains_input(solid_ids, tool_ids, path_id)
5459        || solid2d_id.is_some_and(|solid2d_id| composite_contains_input(solid_ids, tool_ids, solid2d_id))
5460}
5461
5462fn composite_contains_input(solid_ids: &[ArtifactId], tool_ids: &[ArtifactId], input_id: ArtifactId) -> bool {
5463    solid_ids.contains(&input_id) || tool_ids.contains(&input_id)
5464}
5465
5466fn code_ref_source_ref_range(code_ref: &CodeRef) -> SourceRefRange {
5467    let node_path = (!code_ref.node_path.is_empty()).then(|| code_ref.node_path.clone());
5468    SourceRefRange {
5469        range: code_ref.range,
5470        node_path,
5471    }
5472}
5473
5474fn solid_output_index_for_sweep(
5475    artifact_graph: &ArtifactGraph,
5476    sweep_id: ArtifactId,
5477    sweep_code_ref: &CodeRef,
5478) -> Option<usize> {
5479    // Constituent sweeps are implementation details of one composite body,
5480    // even when cloning gives them the same CodeRef as the composite root.
5481    if downstream_composite_id_for_solid_source(artifact_graph, sweep_id).is_some() {
5482        return None;
5483    }
5484
5485    let sibling_sweeps = artifact_graph
5486        .values()
5487        .filter_map(|artifact| match artifact {
5488            Artifact::Sweep(sweep)
5489                if sweep.code_ref.range == sweep_code_ref.range
5490                    && sweep.code_ref.node_path == sweep_code_ref.node_path =>
5491            {
5492                Some(sweep)
5493            }
5494            _ => None,
5495        })
5496        .collect::<Vec<_>>();
5497
5498    if sibling_sweeps.len() <= 1 {
5499        return None;
5500    }
5501
5502    sibling_sweeps
5503        .iter()
5504        .position(|sibling_sweep| sibling_sweep.id == sweep_id)
5505}
5506
5507fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
5508    let mut existing_artifact_ids = scene_objects
5509        .iter()
5510        .map(|object| object.artifact_id)
5511        .collect::<HashSet<_>>();
5512
5513    for artifact in artifact_graph.values() {
5514        match artifact {
5515            Artifact::Wall(wall) => {
5516                if existing_artifact_ids.contains(&wall.id) {
5517                    continue;
5518                }
5519
5520                let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
5521                    Artifact::Segment(segment) => Some(segment),
5522                    _ => None,
5523                }) else {
5524                    continue;
5525                };
5526                let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
5527                    Artifact::Sweep(sweep) => Some(sweep),
5528                    _ => None,
5529                }) else {
5530                    continue;
5531                };
5532                let source_segment = segment
5533                    .original_seg_id
5534                    .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
5535                    .and_then(|artifact| match artifact {
5536                        Artifact::Segment(segment) => Some(segment),
5537                        _ => None,
5538                    })
5539                    .unwrap_or(segment);
5540                let solid_code_ref =
5541                    downstream_composite_code_ref_for_source(artifact_graph, wall.sweep_id).unwrap_or(&sweep.code_ref);
5542                let path_code_ref = artifact_graph
5543                    .get(&segment.path_id)
5544                    .or_else(|| artifact_graph.get(&sweep.path_id))
5545                    .and_then(|artifact| match artifact {
5546                        Artifact::Path(path) => Some(&path.code_ref),
5547                        _ => None,
5548                    });
5549                let source = WallSource {
5550                    solid: code_ref_source_ref_range(solid_code_ref),
5551                    sweep: code_ref_source_ref_range(&sweep.code_ref),
5552                    path: path_code_ref.map(code_ref_source_ref_range),
5553                    segment: code_ref_source_ref_range(&source_segment.code_ref),
5554                };
5555                let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5556                    && solid_code_ref.node_path == sweep.code_ref.node_path)
5557                    .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5558                    .flatten();
5559                let object_source = source_ref_from_source_ref_range(&source.solid);
5560                let id = ObjectId(scene_objects.len());
5561                scene_objects.push(crate::front::Object {
5562                    id,
5563                    kind: ObjectKind::Wall(crate::frontend::api::Wall {
5564                        id,
5565                        source,
5566                        solid_output_index,
5567                    }),
5568                    label: Default::default(),
5569                    comments: Default::default(),
5570                    artifact_id: wall.id,
5571                    source: object_source,
5572                });
5573                existing_artifact_ids.insert(wall.id);
5574            }
5575            Artifact::Cap(cap) => {
5576                if existing_artifact_ids.contains(&cap.id) {
5577                    continue;
5578                }
5579
5580                let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
5581                    Artifact::Sweep(sweep) => Some(sweep),
5582                    _ => None,
5583                }) else {
5584                    continue;
5585                };
5586                let id = ObjectId(scene_objects.len());
5587                let kind = match cap.sub_type {
5588                    CapSubType::Start => crate::frontend::api::CapKind::Start,
5589                    CapSubType::End => crate::frontend::api::CapKind::End,
5590                };
5591                let solid_code_ref =
5592                    downstream_composite_code_ref_for_source(artifact_graph, cap.sweep_id).unwrap_or(&sweep.code_ref);
5593                let source = CapSource {
5594                    solid: code_ref_source_ref_range(solid_code_ref),
5595                    sweep: code_ref_source_ref_range(&sweep.code_ref),
5596                };
5597                let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5598                    && solid_code_ref.node_path == sweep.code_ref.node_path)
5599                    .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5600                    .flatten();
5601                let object_source = source_ref_from_source_ref_range(&source.solid);
5602                scene_objects.push(crate::front::Object {
5603                    id,
5604                    kind: ObjectKind::Cap(crate::frontend::api::Cap {
5605                        id,
5606                        kind,
5607                        source,
5608                        solid_output_index,
5609                    }),
5610                    label: Default::default(),
5611                    comments: Default::default(),
5612                    artifact_id: cap.id,
5613                    source: object_source,
5614                });
5615                existing_artifact_ids.insert(cap.id);
5616            }
5617            _ => {}
5618        }
5619    }
5620}
5621
5622fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
5623    use crate::engine::PlaneName;
5624
5625    match name {
5626        PlaneName::Xy => ast_name_expr("XY".to_owned()),
5627        PlaneName::Xz => ast_name_expr("XZ".to_owned()),
5628        PlaneName::Yz => ast_name_expr("YZ".to_owned()),
5629        PlaneName::NegXy => negated_plane_ast_expr("XY"),
5630        PlaneName::NegXz => negated_plane_ast_expr("XZ"),
5631        PlaneName::NegYz => negated_plane_ast_expr("YZ"),
5632    }
5633}
5634
5635fn negated_plane_ast_expr(name: &str) -> ast::Expr {
5636    ast::Expr::UnaryExpression(BoxNode::new(ast::UnaryExpression::new(
5637        ast::UnaryOperator::Neg,
5638        ast::BinaryPart::Name(BoxNode::new(ast_name(name.to_owned()))),
5639    )))
5640}
5641
5642fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
5643    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
5644        callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
5645        unlabeled: Some(solid_expr),
5646        arguments: vec![ast::LabeledArg {
5647            label: Some(ast::Identifier::new("face")),
5648            arg: face_expr,
5649        }],
5650        digest: None,
5651        non_code_meta: Default::default(),
5652    })))
5653}
5654
5655fn create_face_id_ast(solid_expr: ast::Expr, index: usize) -> ast::Expr {
5656    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
5657        callee: ast::Node::no_src(ast_sketch2_name("faceId")),
5658        unlabeled: Some(solid_expr),
5659        arguments: vec![ast::LabeledArg {
5660            label: Some(ast::Identifier::new("index")),
5661            arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(
5662                ast::NumericLiteral {
5663                    value: index as f64,
5664                    suffix: NumericSuffix::None,
5665                    raw: index.to_string(),
5666                    digest: None,
5667                },
5668            )))),
5669        }],
5670        digest: None,
5671        non_code_meta: Default::default(),
5672    })))
5673}
5674
5675fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
5676    let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
5677        return None;
5678    };
5679    let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
5680        return None;
5681    };
5682    if !matches!(
5683        sweep_call.callee.name.name.as_str(),
5684        "extrude" | "revolve" | "sweep" | "loft"
5685    ) {
5686        return None;
5687    }
5688    let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
5689        return None;
5690    };
5691    let candidate = region_name_expr.name.name.clone();
5692    let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5693        return None;
5694    };
5695    let ast::Expr::CallExpressionKw(region_call) = &region_decl.init else {
5696        return None;
5697    };
5698    if region_call.callee.name.name != "region" {
5699        return None;
5700    }
5701    Some(candidate)
5702}
5703
5704/// Return the AST expression referencing the variable at the given source ref.
5705/// If no such variable exists, insert a new variable declaration with the given
5706/// prefix.
5707///
5708/// This may return a complex expression referencing properties of the variable
5709/// (e.g., `line1.start`).
5710fn get_or_insert_ast_reference(
5711    ast: &mut ast::Node<ast::Program>,
5712    source_ref: &SourceRef,
5713    prefix: &str,
5714    property: Option<&str>,
5715) -> Result<ast::Expr, KclError> {
5716    let command = AstMutateCommand::AddVariableDeclaration {
5717        prefix: prefix.to_owned(),
5718    };
5719    let ret = match mutate_ast_node_by_source_ref(ast, source_ref, command) {
5720        Ok((_, ret)) => ret,
5721        Err(err) => {
5722            if let Some(var_name) = variable_name_containing_source_ref(ast, source_ref) {
5723                AstMutateCommandReturn::Name(var_name)
5724            } else {
5725                return Err(err);
5726            }
5727        }
5728    };
5729    let AstMutateCommandReturn::Name(var_name) = ret else {
5730        return Err(KclError::refactor(
5731            "Expected variable name returned from AddVariableDeclaration".to_owned(),
5732        ));
5733    };
5734    let var_expr = ast::Expr::Name(BoxNode::new(ast::Name::new(&var_name)));
5735    let Some(property) = property else {
5736        // No property; just return the variable name.
5737        return Ok(var_expr);
5738    };
5739
5740    Ok(create_member_expression(var_expr, property))
5741}
5742
5743fn variable_name_containing_source_ref(ast: &ast::Node<ast::Program>, source_ref: &SourceRef) -> Option<String> {
5744    let source_range = match source_ref {
5745        SourceRef::Simple { range, .. } => *range,
5746        SourceRef::BackTrace { ranges } => {
5747            let [range] = ranges.as_slice() else {
5748                return None;
5749            };
5750            range.0
5751        }
5752    };
5753    ast.body.iter().find_map(|item| {
5754        let ast::BodyItem::VariableDeclaration(var_decl) = item else {
5755            return None;
5756        };
5757        let init_range = SourceRange::from(&var_decl.declaration.init);
5758        let source_is_inside_init = init_range.module_id() == source_range.module_id()
5759            && init_range.start() <= source_range.start()
5760            && source_range.end() <= init_range.end();
5761        if matches!(&var_decl.declaration.init, ast::Expr::SketchBlock(_))
5762            && init_range != source_range
5763            && source_is_inside_init
5764        {
5765            return None;
5766        }
5767        source_is_inside_init.then(|| var_decl.name().to_owned())
5768    })
5769}
5770
5771fn mutate_ast_node_by_source_ref(
5772    ast: &mut ast::Node<ast::Program>,
5773    source_ref: &SourceRef,
5774    command: AstMutateCommand,
5775) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5776    let (source_range, node_path) = match source_ref {
5777        SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
5778        SourceRef::BackTrace { ranges } => {
5779            let [range] = ranges.as_slice() else {
5780                return Err(KclError::refactor(format!(
5781                    "Expected single source ref, got {}; ranges={ranges:#?}",
5782                    ranges.len(),
5783                )));
5784            };
5785            (range.0, range.1.clone())
5786        }
5787    };
5788    let mut context = AstMutateContext {
5789        source_range,
5790        node_path,
5791        command,
5792        defined_names_stack: Default::default(),
5793    };
5794    let control = dfs_mut(ast, &mut context);
5795    match control {
5796        ControlFlow::Continue(_) => Err(KclError::refactor(
5797            "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
5798        )),
5799        ControlFlow::Break(break_value) => break_value,
5800    }
5801}
5802
5803#[derive(Debug)]
5804struct AstMutateContext {
5805    source_range: SourceRange,
5806    node_path: Option<ast::NodePath>,
5807    command: AstMutateCommand,
5808    defined_names_stack: Vec<HashSet<String>>,
5809}
5810
5811#[derive(Debug)]
5812#[allow(clippy::large_enum_variant)]
5813enum AstMutateCommand {
5814    /// Add an expression statement to the sketch block.
5815    AddSketchBlockExprStmt {
5816        expr: ast::Expr,
5817    },
5818    /// Add a variable declaration to the sketch block (e.g. `line1 = line(...)`).
5819    AddSketchBlockVarDecl {
5820        prefix: String,
5821        expr: ast::Expr,
5822    },
5823    AddVariableDeclaration {
5824        prefix: String,
5825    },
5826    EditPoint {
5827        at: ast::Expr,
5828    },
5829    EditLine {
5830        start: ast::Expr,
5831        end: ast::Expr,
5832        construction: Option<bool>,
5833    },
5834    EditArc {
5835        start: ast::Expr,
5836        end: ast::Expr,
5837        center: ast::Expr,
5838        direction: Option<ArcDirection>,
5839        construction: Option<bool>,
5840    },
5841    EditCircle {
5842        start: ast::Expr,
5843        center: ast::Expr,
5844        construction: Option<bool>,
5845    },
5846    EditControlPointSpline {
5847        points: ast::Expr,
5848        construction: Option<bool>,
5849    },
5850    EditConstraintValue {
5851        value: ast::BinaryPart,
5852    },
5853    EditAngleConstraint {
5854        call: ast::BinaryPart,
5855        value: ast::BinaryPart,
5856    },
5857    EditDistanceConstraint {
5858        call: ast::BinaryPart,
5859        value: ast::BinaryPart,
5860    },
5861    EditDistanceConstraintLabelPosition {
5862        label_position: ast::Expr,
5863    },
5864    EditCallUnlabeled {
5865        arg: ast::Expr,
5866    },
5867    EditVarInitialValue {
5868        value: Number,
5869    },
5870    DeleteNode,
5871}
5872
5873impl AstMutateCommand {
5874    fn needs_defined_names_stack(&self) -> bool {
5875        matches!(
5876            self,
5877            AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
5878        )
5879    }
5880}
5881
5882#[derive(Debug)]
5883enum AstMutateCommandReturn {
5884    None,
5885    Name(String),
5886}
5887
5888#[derive(Debug, Clone)]
5889struct AstNodeRef {
5890    range: SourceRange,
5891    node_path: Option<ast::NodePath>,
5892}
5893
5894impl<T> From<&ast::Node<T>> for AstNodeRef {
5895    fn from(value: &ast::Node<T>) -> Self {
5896        AstNodeRef {
5897            range: value.into(),
5898            node_path: value.node_path.clone(),
5899        }
5900    }
5901}
5902
5903impl From<&ast::BodyItem> for AstNodeRef {
5904    fn from(value: &ast::BodyItem) -> Self {
5905        match value {
5906            ast::BodyItem::ImportStatement(node) => AstNodeRef {
5907                range: node.into(),
5908                node_path: node.node_path.clone(),
5909            },
5910            ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
5911                range: node.into(),
5912                node_path: node.node_path.clone(),
5913            },
5914            ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
5915                range: node.into(),
5916                node_path: node.node_path.clone(),
5917            },
5918            ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
5919                range: node.into(),
5920                node_path: node.node_path.clone(),
5921            },
5922            ast::BodyItem::ReturnStatement(node) => AstNodeRef {
5923                range: node.into(),
5924                node_path: node.node_path.clone(),
5925            },
5926        }
5927    }
5928}
5929
5930impl From<&ast::Expr> for AstNodeRef {
5931    fn from(value: &ast::Expr) -> Self {
5932        AstNodeRef {
5933            range: SourceRange::from(value),
5934            node_path: value.node_path().cloned(),
5935        }
5936    }
5937}
5938
5939impl From<&AstMutateContext> for AstNodeRef {
5940    fn from(value: &AstMutateContext) -> Self {
5941        AstNodeRef {
5942            range: value.source_range,
5943            node_path: value.node_path.clone(),
5944        }
5945    }
5946}
5947
5948impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5949    type Error = crate::walk::AstNodeError;
5950
5951    fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5952        Ok(AstNodeRef {
5953            range: SourceRange::try_from(value)?,
5954            node_path: value.try_into()?,
5955        })
5956    }
5957}
5958
5959impl From<AstNodeRef> for SourceRange {
5960    fn from(value: AstNodeRef) -> Self {
5961        value.range
5962    }
5963}
5964
5965impl Visitor for AstMutateContext {
5966    type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5967    type Continue = ();
5968
5969    fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5970        filter_and_process(self, node)
5971    }
5972
5973    fn finish(&mut self, node: NodeMut<'_>) {
5974        match &node {
5975            NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
5976                self.defined_names_stack.pop();
5977            }
5978            _ => {}
5979        }
5980    }
5981}
5982
5983fn filter_and_process(
5984    ctx: &mut AstMutateContext,
5985    node: NodeMut,
5986) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
5987    let Ok(node_range) = SourceRange::try_from(&node) else {
5988        // Nodes that can't be converted to a range aren't interesting.
5989        return TraversalReturn::new_continue(());
5990    };
5991    // If we're adding a variable declaration, we need to look at variable
5992    // declaration expressions to see if it already has a variable, before
5993    // continuing. The variable declaration's source range won't match the
5994    // target; its init expression will.
5995    if let NodeMut::VariableDeclaration(var_decl) = &node {
5996        let expr_range = SourceRange::from(&var_decl.declaration.init);
5997        let expr_node_path = var_decl.declaration.init.node_path();
5998        if source_ref_matches(ctx, expr_range, expr_node_path) {
5999            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
6000                // We found the variable declaration expression. It doesn't need
6001                // to be added.
6002                return TraversalReturn::new_break(Ok((
6003                    AstNodeRef::from(&**var_decl),
6004                    AstMutateCommandReturn::Name(var_decl.name().to_owned()),
6005                )));
6006            }
6007            if let AstMutateCommand::DeleteNode = &ctx.command {
6008                // We found the variable declaration. Delete the variable along
6009                // with the segment.
6010                return TraversalReturn {
6011                    mutate_body_item: MutateBodyItem::Delete,
6012                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
6013                };
6014            }
6015        }
6016    }
6017    // Similar thing with expression statement. We need to look at the
6018    // expression inside it.
6019    if let NodeMut::ExpressionStatement(expr_stmt) = &node {
6020        let expr_range = SourceRange::from(&expr_stmt.expression);
6021        let expr_node_path = expr_stmt.expression.node_path();
6022        if source_ref_matches(ctx, expr_range, expr_node_path) {
6023            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
6024                // We found the node wrapped in an expression statement. Process
6025                // the statement.
6026                let Ok(node_ref) = AstNodeRef::try_from(&node) else {
6027                    return TraversalReturn::new_continue(());
6028                };
6029                return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
6030            }
6031            if let AstMutateCommand::DeleteNode = &ctx.command {
6032                // We found the node wrapped in an expression statement. Delete
6033                // the whole statement.
6034                return TraversalReturn {
6035                    mutate_body_item: MutateBodyItem::Delete,
6036                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
6037                };
6038            }
6039        }
6040    }
6041
6042    if ctx.command.needs_defined_names_stack() {
6043        if let NodeMut::Program(program) = &node {
6044            ctx.defined_names_stack.push(find_defined_names(*program));
6045        } else if let NodeMut::SketchBlock(block) = &node {
6046            ctx.defined_names_stack.push(find_defined_names(&block.body));
6047        }
6048    }
6049
6050    // Make sure the node matches the source ref.
6051    let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
6052    if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
6053        return TraversalReturn::new_continue(());
6054    }
6055    let Ok(node_ref) = AstNodeRef::try_from(&node) else {
6056        return TraversalReturn::new_continue(());
6057    };
6058    process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
6059}
6060
6061fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
6062    match &ctx.node_path {
6063        Some(target) => Some(target) == node_path,
6064        None => node_range == ctx.source_range,
6065    }
6066}
6067
6068fn is_angle_constraint_call_name(name: &str) -> bool {
6069    matches!(name, ANGLE_FN | ANGLE_DIMENSION_FN)
6070}
6071
6072fn is_distance_constraint_call_name(name: &str) -> bool {
6073    matches!(name, DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN)
6074}
6075
6076fn is_constraint_call_name(name: &str) -> bool {
6077    matches!(
6078        name,
6079        DISTANCE_FN
6080            | HORIZONTAL_DISTANCE_FN
6081            | VERTICAL_DISTANCE_FN
6082            | RADIUS_FN
6083            | DIAMETER_FN
6084            | ANGLE_FN
6085            | ANGLE_DIMENSION_FN
6086    )
6087}
6088
6089fn constraint_supports_label_position(part: &mut ast::BinaryPart) -> Option<&mut BoxNode<CallExpressionKw>> {
6090    if let ast::BinaryPart::CallExpressionKw(call) = part
6091        && is_constraint_call_name(call.callee.name.name.as_str())
6092    {
6093        Some(call)
6094    } else {
6095        None
6096    }
6097}
6098
6099fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
6100    match &ctx.command {
6101        AstMutateCommand::AddSketchBlockExprStmt { expr } => {
6102            if let NodeMut::SketchBlock(sketch_block) = node {
6103                sketch_block
6104                    .body
6105                    .items
6106                    .push(ast::BodyItem::ExpressionStatement(ast::Node {
6107                        inner: ast::ExpressionStatement {
6108                            expression: expr.clone(),
6109                            digest: None,
6110                        },
6111                        start: Default::default(),
6112                        end: Default::default(),
6113                        module_id: Default::default(),
6114                        node_path: None,
6115                        outer_attrs: Default::default(),
6116                        pre_comments: Default::default(),
6117                        comment_start: Default::default(),
6118                    }));
6119                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6120            }
6121        }
6122        AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
6123            if let NodeMut::SketchBlock(sketch_block) = node {
6124                let empty_defined_names = HashSet::new();
6125                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6126                let Ok(name) = next_free_name(prefix, defined_names) else {
6127                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6128                };
6129                sketch_block
6130                    .body
6131                    .items
6132                    .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
6133                        ast::VariableDeclaration::new(
6134                            ast::VariableDeclarator::new(&name, expr.clone()),
6135                            ast::ItemVisibility::Default,
6136                            ast::VariableKind::Const,
6137                        ),
6138                    ))));
6139                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
6140            }
6141        }
6142        AstMutateCommand::AddVariableDeclaration { prefix } => {
6143            if let NodeMut::VariableDeclaration(inner) = node {
6144                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
6145            }
6146            if let NodeMut::ExpressionStatement(expr_stmt) = node {
6147                let empty_defined_names = HashSet::new();
6148                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6149                let Ok(name) = next_free_name(prefix, defined_names) else {
6150                    // TODO: Return an error instead?
6151                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6152                };
6153                let mutate_node =
6154                    ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(ast::VariableDeclaration::new(
6155                        ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
6156                        ast::ItemVisibility::Default,
6157                        ast::VariableKind::Const,
6158                    ))));
6159                return TraversalReturn {
6160                    mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
6161                    control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
6162                };
6163            }
6164        }
6165        AstMutateCommand::EditPoint { at } => {
6166            if let NodeMut::CallExpressionKw(call) = node {
6167                if call.callee.name.name != POINT_FN {
6168                    return TraversalReturn::new_continue(());
6169                }
6170                // Update the arguments.
6171                for labeled_arg in &mut call.arguments {
6172                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
6173                        labeled_arg.arg = at.clone();
6174                    }
6175                }
6176                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6177            }
6178        }
6179        AstMutateCommand::EditLine {
6180            start,
6181            end,
6182            construction,
6183        } => {
6184            if let NodeMut::CallExpressionKw(call) = node {
6185                if call.callee.name.name != LINE_FN {
6186                    return TraversalReturn::new_continue(());
6187                }
6188                // Update the arguments.
6189                for labeled_arg in &mut call.arguments {
6190                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
6191                        labeled_arg.arg = start.clone();
6192                    }
6193                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
6194                        labeled_arg.arg = end.clone();
6195                    }
6196                }
6197                // Handle construction kwarg
6198                if let Some(construction_value) = construction {
6199                    let construction_exists = call
6200                        .arguments
6201                        .iter()
6202                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6203                    if *construction_value {
6204                        // Add or update construction=true
6205                        if construction_exists {
6206                            // Update existing construction kwarg
6207                            for labeled_arg in &mut call.arguments {
6208                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6209                                    labeled_arg.arg =
6210                                        ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6211                                            value: ast::LiteralValue::Bool(true),
6212                                            raw: "true".to_string(),
6213                                            digest: None,
6214                                        })));
6215                                }
6216                            }
6217                        } else {
6218                            // Add new construction kwarg
6219                            call.arguments.push(ast::LabeledArg {
6220                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6221                                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6222                                    value: ast::LiteralValue::Bool(true),
6223                                    raw: "true".to_string(),
6224                                    digest: None,
6225                                }))),
6226                            });
6227                        }
6228                    } else {
6229                        // Remove construction kwarg if it exists
6230                        call.arguments
6231                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6232                    }
6233                }
6234                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6235            }
6236        }
6237        AstMutateCommand::EditArc {
6238            start,
6239            end,
6240            center,
6241            direction,
6242            construction,
6243        } => {
6244            if let NodeMut::CallExpressionKw(call) = node {
6245                if call.callee.name.name != ARC_FN {
6246                    return TraversalReturn::new_continue(());
6247                }
6248                // Update the arguments.
6249                for labeled_arg in &mut call.arguments {
6250                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
6251                        labeled_arg.arg = start.clone();
6252                    }
6253                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
6254                        labeled_arg.arg = end.clone();
6255                    }
6256                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
6257                        labeled_arg.arg = center.clone();
6258                    }
6259                }
6260                // Handle direction kwarg
6261                if let Some(direction_value) = direction {
6262                    let direction_exists = call
6263                        .arguments
6264                        .iter()
6265                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_DIRECTION_PARAM));
6266                    if direction_value.is_clockwise() {
6267                        let direction_ast = ast::Expr::Name(BoxNode::new(ast::Name::new(ARC_DIRECTION_CW_NAME)));
6268                        if direction_exists {
6269                            // Update existing direction kwarg
6270                            for labeled_arg in &mut call.arguments {
6271                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_DIRECTION_PARAM) {
6272                                    labeled_arg.arg = direction_ast.clone();
6273                                }
6274                            }
6275                        } else {
6276                            // Add new direction kwarg
6277                            call.arguments.push(ast::LabeledArg {
6278                                label: Some(ast::Identifier::new(ARC_DIRECTION_PARAM)),
6279                                arg: direction_ast,
6280                            });
6281                        }
6282                    } else {
6283                        // Remove direction kwarg if it exists since
6284                        // counterclockwise is the default
6285                        call.arguments
6286                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(ARC_DIRECTION_PARAM));
6287                    }
6288                }
6289                // Handle construction kwarg
6290                if let Some(construction_value) = construction {
6291                    let construction_exists = call
6292                        .arguments
6293                        .iter()
6294                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6295                    if *construction_value {
6296                        // Add or update construction=true
6297                        if construction_exists {
6298                            // Update existing construction kwarg
6299                            for labeled_arg in &mut call.arguments {
6300                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6301                                    labeled_arg.arg =
6302                                        ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6303                                            value: ast::LiteralValue::Bool(true),
6304                                            raw: "true".to_string(),
6305                                            digest: None,
6306                                        })));
6307                                }
6308                            }
6309                        } else {
6310                            // Add new construction kwarg
6311                            call.arguments.push(ast::LabeledArg {
6312                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6313                                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6314                                    value: ast::LiteralValue::Bool(true),
6315                                    raw: "true".to_string(),
6316                                    digest: None,
6317                                }))),
6318                            });
6319                        }
6320                    } else {
6321                        // Remove construction kwarg if it exists
6322                        call.arguments
6323                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6324                    }
6325                }
6326                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6327            }
6328        }
6329        AstMutateCommand::EditCircle {
6330            start,
6331            center,
6332            construction,
6333        } => {
6334            if let NodeMut::CallExpressionKw(call) = node {
6335                if call.callee.name.name != CIRCLE_FN {
6336                    return TraversalReturn::new_continue(());
6337                }
6338                // Update the arguments.
6339                for labeled_arg in &mut call.arguments {
6340                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
6341                        labeled_arg.arg = start.clone();
6342                    }
6343                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
6344                        labeled_arg.arg = center.clone();
6345                    }
6346                }
6347                // Handle construction kwarg
6348                if let Some(construction_value) = construction {
6349                    let construction_exists = call
6350                        .arguments
6351                        .iter()
6352                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6353                    if *construction_value {
6354                        if construction_exists {
6355                            // Update existing construction kwarg
6356                            for labeled_arg in &mut call.arguments {
6357                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6358                                    labeled_arg.arg =
6359                                        ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6360                                            value: ast::LiteralValue::Bool(true),
6361                                            raw: "true".to_string(),
6362                                            digest: None,
6363                                        })));
6364                                }
6365                            }
6366                        } else {
6367                            // Add new construction kwarg
6368                            call.arguments.push(ast::LabeledArg {
6369                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6370                                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6371                                    value: ast::LiteralValue::Bool(true),
6372                                    raw: "true".to_string(),
6373                                    digest: None,
6374                                }))),
6375                            });
6376                        }
6377                    } else {
6378                        // Remove construction kwarg if it exists
6379                        call.arguments
6380                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6381                    }
6382                }
6383                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6384            }
6385        }
6386        AstMutateCommand::EditControlPointSpline { points, construction } => {
6387            if let NodeMut::CallExpressionKw(call) = node {
6388                if call.callee.name.name != CONTROL_POINT_SPLINE_FN {
6389                    return TraversalReturn::new_continue(());
6390                }
6391                for labeled_arg in &mut call.arguments {
6392                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONTROL_POINT_SPLINE_POINTS_PARAM)
6393                    {
6394                        labeled_arg.arg = points.clone();
6395                    }
6396                }
6397                // Handle construction kwarg
6398                if let Some(construction_value) = construction {
6399                    let construction_exists = call
6400                        .arguments
6401                        .iter()
6402                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6403                    if *construction_value {
6404                        if construction_exists {
6405                            for labeled_arg in &mut call.arguments {
6406                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6407                                    labeled_arg.arg =
6408                                        ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6409                                            value: ast::LiteralValue::Bool(true),
6410                                            raw: "true".to_string(),
6411                                            digest: None,
6412                                        })));
6413                                }
6414                            }
6415                        } else {
6416                            call.arguments.push(ast::LabeledArg {
6417                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6418                                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6419                                    value: ast::LiteralValue::Bool(true),
6420                                    raw: "true".to_string(),
6421                                    digest: None,
6422                                }))),
6423                            });
6424                        }
6425                    } else {
6426                        call.arguments
6427                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6428                    }
6429                }
6430                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6431            }
6432        }
6433        AstMutateCommand::EditConstraintValue { value } => {
6434            if let NodeMut::BinaryExpression(binary_expr) = node {
6435                let left_is_constraint = matches!(
6436                    &binary_expr.left,
6437                    ast::BinaryPart::CallExpressionKw(call) if is_constraint_call_name(call.callee.name.name.as_str())
6438                );
6439                if left_is_constraint {
6440                    binary_expr.right = value.clone();
6441                } else {
6442                    binary_expr.left = value.clone();
6443                }
6444
6445                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6446            }
6447        }
6448        AstMutateCommand::EditAngleConstraint { call, value } => {
6449            if let NodeMut::BinaryExpression(binary_expr) = node {
6450                let left_is_angle = matches!(
6451                    &binary_expr.left,
6452                    ast::BinaryPart::CallExpressionKw(existing_call)
6453                        if is_angle_constraint_call_name(existing_call.callee.name.name.as_str())
6454                );
6455                let right_is_angle = matches!(
6456                    &binary_expr.right,
6457                    ast::BinaryPart::CallExpressionKw(existing_call)
6458                        if is_angle_constraint_call_name(existing_call.callee.name.name.as_str())
6459                );
6460
6461                match (left_is_angle, right_is_angle) {
6462                    (true, _) => {
6463                        binary_expr.left = call.clone();
6464                        binary_expr.right = value.clone();
6465                    }
6466                    (false, true) => {
6467                        binary_expr.left = value.clone();
6468                        binary_expr.right = call.clone();
6469                    }
6470                    (false, false) => return TraversalReturn::new_continue(()),
6471                }
6472
6473                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6474            }
6475        }
6476        AstMutateCommand::EditDistanceConstraint { call, value } => {
6477            if let NodeMut::BinaryExpression(binary_expr) = node {
6478                let left_is_distance = matches!(
6479                    &binary_expr.left,
6480                    ast::BinaryPart::CallExpressionKw(existing_call)
6481                        if is_distance_constraint_call_name(existing_call.callee.name.name.as_str())
6482                );
6483                let right_is_distance = matches!(
6484                    &binary_expr.right,
6485                    ast::BinaryPart::CallExpressionKw(existing_call)
6486                        if is_distance_constraint_call_name(existing_call.callee.name.name.as_str())
6487                );
6488
6489                match (left_is_distance, right_is_distance) {
6490                    (true, _) => {
6491                        binary_expr.left = call.clone();
6492                        binary_expr.right = value.clone();
6493                    }
6494                    (false, true) => {
6495                        binary_expr.left = value.clone();
6496                        binary_expr.right = call.clone();
6497                    }
6498                    (false, false) => return TraversalReturn::new_continue(()),
6499                }
6500
6501                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6502            }
6503        }
6504        AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
6505            if let NodeMut::BinaryExpression(binary_expr) = node {
6506                let call = if let Some(call) = constraint_supports_label_position(&mut binary_expr.left) {
6507                    call
6508                } else if let Some(call) = constraint_supports_label_position(&mut binary_expr.right) {
6509                    call
6510                } else {
6511                    return TraversalReturn::new_continue(());
6512                };
6513
6514                if let Some(label_arg) = call
6515                    .arguments
6516                    .iter_mut()
6517                    .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
6518                {
6519                    label_arg.arg = label_position.clone();
6520                } else {
6521                    call.arguments.push(ast::LabeledArg {
6522                        label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
6523                        arg: label_position.clone(),
6524                    });
6525                }
6526
6527                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6528            }
6529        }
6530        AstMutateCommand::EditCallUnlabeled { arg } => {
6531            if let NodeMut::CallExpressionKw(call) = node {
6532                call.unlabeled = Some(arg.clone());
6533                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6534            }
6535        }
6536        AstMutateCommand::EditVarInitialValue { value } => {
6537            // We target the SketchVar itself (matched by NodePath) rather than
6538            // the inner NumericLiteral so we can also write back into vars that
6539            // were declared without an initial value (e.g. bare `var`).
6540            if let NodeMut::SketchVar(sketch_var) = node {
6541                let Ok(literal) = to_source_number(*value) else {
6542                    return TraversalReturn::new_break(Err(KclError::refactor(format!(
6543                        "Could not convert number to AST literal: {:?}",
6544                        *value
6545                    ))));
6546                };
6547                sketch_var.initial = Some(BoxNode::new(ast::Node::no_src(literal)));
6548                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6549            }
6550        }
6551        AstMutateCommand::DeleteNode => {
6552            return TraversalReturn {
6553                mutate_body_item: MutateBodyItem::Delete,
6554                control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
6555            };
6556        }
6557    }
6558    TraversalReturn::new_continue(())
6559}
6560
6561struct FindSketchBlockSourceRange {
6562    /// The source range of the sketch block before mutation.
6563    target_before_mutation: SourceRange,
6564    /// The source range of the sketch block's last body item after mutation. We
6565    /// need to use a [Cell] since the [crate::walk::Visitor] trait requires a
6566    /// shared reference.
6567    found: Cell<Option<AstNodeRef>>,
6568}
6569
6570impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
6571    type Error = crate::front::Error;
6572
6573    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6574        let Ok(node_range) = SourceRange::try_from(&node) else {
6575            return Ok(true);
6576        };
6577
6578        if let crate::walk::Node::SketchBlock(sketch_block) = node {
6579            if node_range.module_id() == self.target_before_mutation.module_id()
6580                && node_range.start() == self.target_before_mutation.start()
6581                // End shouldn't match since we added something.
6582                && node_range.end() >= self.target_before_mutation.end()
6583            {
6584                self.found.set(sketch_block.body.items.last().map(|item| match item {
6585                    // For declarations like `circle1 = circle(...)`, use
6586                    // the init expression range so lookup in source_range_to_object
6587                    // matches the segment source range.
6588                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6589                    _ => AstNodeRef::from(item),
6590                }));
6591                return Ok(false);
6592            } else {
6593                // We found a different sketch block. No need to descend into
6594                // its children since sketch blocks cannot be nested.
6595                return Ok(true);
6596            }
6597        }
6598
6599        for child in node.children().iter() {
6600            if !child.visit(*self)? {
6601                return Ok(false);
6602            }
6603        }
6604
6605        Ok(true)
6606    }
6607}
6608
6609struct FindSketchBlockByNodePath {
6610    /// The Node Path of the sketch block before mutation.
6611    target_node_path: ast::NodePath,
6612    /// The ref of the sketch block's last body item after mutation. We need to
6613    /// use a [Cell] since the [crate::walk::Visitor] trait requires a shared
6614    /// reference.
6615    found: Cell<Option<AstNodeRef>>,
6616}
6617
6618impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
6619    type Error = crate::front::Error;
6620
6621    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6622        let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
6623            return Ok(true);
6624        };
6625
6626        if let crate::walk::Node::SketchBlock(sketch_block) = node {
6627            if let Some(node_path) = node_path
6628                && node_path == self.target_node_path
6629            {
6630                self.found.set(sketch_block.body.items.last().map(|item| match item {
6631                    // For declarations like `circle1 = circle(...)`, use
6632                    // the init expression range so lookup in source_range_to_object
6633                    // matches the segment source range.
6634                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6635                    _ => AstNodeRef::from(item),
6636                }));
6637
6638                return Ok(false);
6639            } else {
6640                // We found a different sketch block. No need to descend into
6641                // its children since sketch blocks cannot be nested.
6642                return Ok(true);
6643            }
6644        }
6645
6646        for child in node.children().iter() {
6647            if !child.visit(*self)? {
6648                return Ok(false);
6649            }
6650        }
6651
6652        Ok(true)
6653    }
6654}
6655
6656/// After adding an item to a sketch block, find the sketch block, and get the
6657/// source range of the added item. We assume that the added item is the last
6658/// item in the sketch block and that the sketch block's source range has grown,
6659/// but not moved from its starting offset.
6660///
6661/// TODO: Do we need to format *before* mutation in case formatting moves the
6662/// sketch block forward?
6663fn find_sketch_block_added_item(
6664    ast: &ast::Node<ast::Program>,
6665    sketch_block_before_mutation: &AstNodeRef,
6666) -> Result<AstNodeRef, KclError> {
6667    if let Some(node_path) = &sketch_block_before_mutation.node_path {
6668        let find = FindSketchBlockByNodePath {
6669            target_node_path: node_path.clone(),
6670            found: Cell::new(None),
6671        };
6672        let node = crate::walk::Node::from(ast);
6673        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6674        find.found.into_inner().ok_or_else(|| {
6675            KclError::refactor(format!(
6676                "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
6677            ))
6678        })
6679    } else {
6680        // No NodePath. Fall back to legacy source range.
6681        let find = FindSketchBlockSourceRange {
6682            target_before_mutation: sketch_block_before_mutation.range,
6683            found: Cell::new(None),
6684        };
6685        let node = crate::walk::Node::from(ast);
6686        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6687        find.found.into_inner().ok_or_else(|| KclError::refactor(
6688            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?"),
6689        ))
6690    }
6691}
6692
6693fn format_kcl_error_message(prefix: &str, error: &KclError) -> String {
6694    let message = error.message().trim();
6695    let message = if message.is_empty() {
6696        "unknown parse error"
6697    } else {
6698        message
6699    };
6700
6701    format!("{prefix}: {message}")
6702}
6703
6704fn parse_frontend_mutation_source(source: &str, parse_error_prefix: &str, no_ast_message: &str) -> ExecResult<Program> {
6705    let (program, errors) = Program::parse(source).map_err(|err| {
6706        KclErrorWithOutputs::no_outputs(KclError::refactor(format_kcl_error_message(parse_error_prefix, &err)))
6707    })?;
6708    if !errors.is_empty() {
6709        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
6710            format_compilation_issues(parse_error_prefix, &errors),
6711        )));
6712    }
6713
6714    program.ok_or_else(|| KclErrorWithOutputs::no_outputs(KclError::refactor(no_ast_message.to_owned())))
6715}
6716
6717fn format_compilation_issues(prefix: &str, issues: &[CompilationIssue]) -> String {
6718    let Some(first_issue) = issues
6719        .iter()
6720        .find(|issue| issue.severity.is_err())
6721        .or_else(|| issues.first())
6722    else {
6723        return prefix.to_owned();
6724    };
6725
6726    let message = first_issue.message.trim();
6727    let message = if message.is_empty() {
6728        "unknown parse error"
6729    } else {
6730        message
6731    };
6732
6733    if issues.len() > 1 {
6734        format!("{prefix}: {message} (+{} more)", issues.len() - 1)
6735    } else {
6736        format!("{prefix}: {message}")
6737    }
6738}
6739
6740fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
6741    // TODO: Don't duplicate this from lib.rs Program.
6742    ast.recast_top(&Default::default(), 0)
6743}
6744
6745struct FindNumericLiteral {
6746    target: SourceRange,
6747    found: Cell<Option<ast::NumericLiteral>>,
6748}
6749
6750impl<'a> crate::walk::Visitor<'a> for &FindNumericLiteral {
6751    type Error = crate::front::Error;
6752
6753    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6754        let Ok(node_range) = SourceRange::try_from(&node) else {
6755            return Ok(true);
6756        };
6757
6758        if node_range == self.target
6759            && let crate::walk::Node::NumericLiteral(literal) = node
6760        {
6761            self.found.set(Some(literal.inner.clone()));
6762            return Ok(false);
6763        }
6764
6765        for child in node.children().iter() {
6766            if !child.visit(*self)? {
6767                return Ok(false);
6768            }
6769        }
6770
6771        Ok(true)
6772    }
6773}
6774
6775fn numeric_literal_at_source_range(ast: &ast::Node<ast::Program>, target: SourceRange) -> Option<ast::NumericLiteral> {
6776    let find = FindNumericLiteral {
6777        target,
6778        found: Cell::new(None),
6779    };
6780    let node = crate::walk::Node::from(ast);
6781    node.visit(&find).ok()?;
6782    find.found.into_inner()
6783}
6784
6785struct FindSketchVarInitialByNodePath<'a> {
6786    target: &'a ast::NodePath,
6787    sketch_var_found: Cell<bool>,
6788    initial_literal: Cell<Option<ast::NumericLiteral>>,
6789}
6790
6791impl<'a, 'b> crate::walk::Visitor<'b> for &FindSketchVarInitialByNodePath<'a> {
6792    type Error = crate::front::Error;
6793
6794    fn visit_node(&self, node: crate::walk::Node<'b>) -> anyhow::Result<bool, Self::Error> {
6795        if let crate::walk::Node::SketchVar(sketch_var) = node
6796            && sketch_var.node_path.as_ref() == Some(self.target)
6797        {
6798            self.sketch_var_found.set(true);
6799            if let Some(initial) = &sketch_var.initial {
6800                self.initial_literal.set(Some(initial.inner.clone()));
6801            }
6802            return Ok(false);
6803        }
6804
6805        for child in node.children().iter() {
6806            if !child.visit(*self)? {
6807                return Ok(false);
6808            }
6809        }
6810
6811        Ok(true)
6812    }
6813}
6814
6815/// Locate the source `var` declaration corresponding to a sketch-var solution.
6816///
6817/// The outer [`Option`] distinguishes "no matching target" (commit must fail)
6818/// from "target found." The inner [`Option`] is the initial numeric literal of
6819/// the [`SketchVar`], if any; bare `var` declarations return `Some(None)`.
6820///
6821/// When `node_path` is `None` (e.g. for older outcomes that predate the
6822/// node-path propagation), this falls back to source-range matching, which
6823/// can break under whitespace shifts elsewhere in the file.
6824fn numeric_literal_at_node_path(
6825    ast: &ast::Node<ast::Program>,
6826    node_path: Option<&ast::NodePath>,
6827    source_range: SourceRange,
6828) -> Option<Option<ast::NumericLiteral>> {
6829    let Some(node_path) = node_path else {
6830        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";
6831        #[cfg(target_arch = "wasm32")]
6832        web_sys::console::warn_1(&message.into());
6833        #[cfg(not(target_arch = "wasm32"))]
6834        eprintln!("WARNING: {message}");
6835        return numeric_literal_at_source_range(ast, source_range).map(Some);
6836    };
6837    let find = FindSketchVarInitialByNodePath {
6838        target: node_path,
6839        sketch_var_found: Cell::new(false),
6840        initial_literal: Cell::new(None),
6841    };
6842    let node = crate::walk::Node::from(ast);
6843    node.visit(&find).ok()?;
6844    if !find.sketch_var_found.get() {
6845        return None;
6846    }
6847    Some(find.initial_literal.into_inner())
6848}
6849
6850fn suffix_length_unit(suffix: NumericSuffix) -> Option<UnitLength> {
6851    match suffix {
6852        NumericSuffix::Mm => Some(UnitLength::Millimeters),
6853        NumericSuffix::Cm => Some(UnitLength::Centimeters),
6854        NumericSuffix::M => Some(UnitLength::Meters),
6855        NumericSuffix::Inch => Some(UnitLength::Inches),
6856        NumericSuffix::Ft => Some(UnitLength::Feet),
6857        NumericSuffix::Yd => Some(UnitLength::Yards),
6858        _ => None,
6859    }
6860}
6861
6862fn number_value_in_default_length_units(number: Number, default_length_unit: UnitLength) -> f64 {
6863    match suffix_length_unit(number.units) {
6864        Some(unit) => adjust_length(unit, number.value, default_length_unit).0,
6865        None => number.value,
6866    }
6867}
6868
6869fn literal_value_in_default_length_units(literal: &ast::NumericLiteral, default_length_unit: UnitLength) -> f64 {
6870    match suffix_length_unit(literal.suffix) {
6871        Some(unit) => adjust_length(unit, literal.value, default_length_unit).0,
6872        None => literal.value,
6873    }
6874}
6875
6876fn var_solution_needs_commit(
6877    current_literal: &ast::NumericLiteral,
6878    solved_value: Number,
6879    default_length_unit: UnitLength,
6880) -> bool {
6881    let current = literal_value_in_default_length_units(current_literal, default_length_unit);
6882    let solved = number_value_in_default_length_units(solved_value, default_length_unit);
6883
6884    (current - solved).abs() > 1e-9
6885}
6886
6887fn preserve_var_solution_literal_style(
6888    current_literal: &ast::NumericLiteral,
6889    solved_value: Number,
6890    default_length_unit: UnitLength,
6891) -> Number {
6892    if current_literal.suffix == NumericSuffix::None {
6893        return Number {
6894            value: number_value_in_default_length_units(solved_value, default_length_unit),
6895            units: NumericSuffix::None,
6896        };
6897    }
6898
6899    let Some(current_unit) = suffix_length_unit(current_literal.suffix) else {
6900        return solved_value;
6901    };
6902
6903    let solved_default_value = number_value_in_default_length_units(solved_value, default_length_unit);
6904    Number {
6905        value: adjust_length(default_length_unit, solved_default_value, current_unit).0,
6906        units: current_literal.suffix,
6907    }
6908}
6909
6910pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
6911    Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node {
6912        inner: ast::ArrayExpression {
6913            elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
6914            non_code_meta: Default::default(),
6915            digest: None,
6916        },
6917        start: Default::default(),
6918        end: Default::default(),
6919        module_id: Default::default(),
6920        node_path: None,
6921        outer_attrs: Default::default(),
6922        pre_comments: Default::default(),
6923        comment_start: Default::default(),
6924    })))
6925}
6926
6927pub(crate) fn to_ast_point2d_array(points: &[Point2d<Expr>]) -> anyhow::Result<ast::Expr> {
6928    Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
6929        ast::ArrayExpression {
6930            elements: points.iter().map(to_ast_point2d).collect::<anyhow::Result<Vec<_>>>()?,
6931            digest: None,
6932            non_code_meta: Default::default(),
6933        },
6934    ))))
6935}
6936
6937fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
6938    Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
6939        ast::ArrayExpression {
6940            elements: vec![
6941                ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6942                    point.x,
6943                )?)))),
6944                ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6945                    point.y,
6946                )?)))),
6947            ],
6948            non_code_meta: Default::default(),
6949            digest: None,
6950        },
6951    ))))
6952}
6953
6954fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
6955    match expr {
6956        Expr::Number(number) => Ok(ast::Expr::Literal(BoxNode::new(ast::Node {
6957            inner: ast::Literal::from(to_source_number(*number)?),
6958            start: Default::default(),
6959            end: Default::default(),
6960            module_id: Default::default(),
6961            node_path: None,
6962            outer_attrs: Default::default(),
6963            pre_comments: Default::default(),
6964            comment_start: Default::default(),
6965        }))),
6966        Expr::Var(number) => Ok(ast::Expr::SketchVar(BoxNode::new(ast::Node {
6967            inner: ast::SketchVar {
6968                initial: Some(BoxNode::new(ast::Node {
6969                    inner: 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                digest: None,
6979            },
6980            start: Default::default(),
6981            end: Default::default(),
6982            module_id: Default::default(),
6983            node_path: None,
6984            outer_attrs: Default::default(),
6985            pre_comments: Default::default(),
6986            comment_start: Default::default(),
6987        }))),
6988        Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
6989    }
6990}
6991
6992fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
6993    Ok(ast::NumericLiteral {
6994        value: number.value,
6995        suffix: number.units,
6996        raw: format_number_literal(number.value, number.units, None)?,
6997        digest: None,
6998    })
6999}
7000
7001pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
7002    ast::Expr::Name(BoxNode::new(ast_name(name)))
7003}
7004
7005fn ast_name(name: String) -> ast::Node<ast::Name> {
7006    ast::Node {
7007        inner: ast::Name {
7008            name: ast::Node {
7009                inner: ast::Identifier { name, digest: None },
7010                start: Default::default(),
7011                end: Default::default(),
7012                module_id: Default::default(),
7013                node_path: None,
7014                outer_attrs: Default::default(),
7015                pre_comments: Default::default(),
7016                comment_start: Default::default(),
7017            },
7018            path: Vec::new(),
7019            abs_path: false,
7020            digest: None,
7021        },
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}
7031
7032pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
7033    ast::Name {
7034        name: ast::Node {
7035            inner: ast::Identifier {
7036                name: name.to_owned(),
7037                digest: None,
7038            },
7039            start: Default::default(),
7040            end: Default::default(),
7041            module_id: Default::default(),
7042            node_path: None,
7043            outer_attrs: Default::default(),
7044            pre_comments: Default::default(),
7045            comment_start: Default::default(),
7046        },
7047        path: Default::default(),
7048        abs_path: false,
7049        digest: None,
7050    }
7051}
7052
7053// Shared AST creation helpers used by both frontend and transpiler to ensure consistency.
7054
7055/// Create an AST node for coincident([expr1, expr2, ...])
7056pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
7057    let elements = exprs.into_iter().collect::<Vec<_>>();
7058    debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
7059
7060    // Create array [expr1, expr2, ...]
7061    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7062        elements,
7063        digest: None,
7064        non_code_meta: Default::default(),
7065    })));
7066
7067    // Create coincident([...])
7068    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7069        callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
7070        unlabeled: Some(array_expr),
7071        arguments: Default::default(),
7072        digest: None,
7073        non_code_meta: Default::default(),
7074    })))
7075}
7076
7077/// Create an AST node for line(start = [...], end = [...])
7078pub(crate) fn create_line_ast(start_ast: ast::Expr, end_ast: ast::Expr) -> ast::Expr {
7079    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7080        callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
7081        unlabeled: None,
7082        arguments: vec![
7083            ast::LabeledArg {
7084                label: Some(ast::Identifier::new(LINE_START_PARAM)),
7085                arg: start_ast,
7086            },
7087            ast::LabeledArg {
7088                label: Some(ast::Identifier::new(LINE_END_PARAM)),
7089                arg: end_ast,
7090            },
7091        ],
7092        digest: None,
7093        non_code_meta: Default::default(),
7094    })))
7095}
7096
7097/// Create an AST node for arc(start = [...], end = [...], center = [...])
7098pub(crate) fn create_arc_ast(start_ast: ast::Expr, end_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
7099    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7100        callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
7101        unlabeled: None,
7102        arguments: vec![
7103            ast::LabeledArg {
7104                label: Some(ast::Identifier::new(ARC_START_PARAM)),
7105                arg: start_ast,
7106            },
7107            ast::LabeledArg {
7108                label: Some(ast::Identifier::new(ARC_END_PARAM)),
7109                arg: end_ast,
7110            },
7111            ast::LabeledArg {
7112                label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
7113                arg: center_ast,
7114            },
7115        ],
7116        digest: None,
7117        non_code_meta: Default::default(),
7118    })))
7119}
7120
7121/// Create an AST node for circle(start = [...], center = [...])
7122pub(crate) fn create_circle_ast(start_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
7123    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7124        callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
7125        unlabeled: None,
7126        arguments: vec![
7127            ast::LabeledArg {
7128                label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
7129                arg: start_ast,
7130            },
7131            ast::LabeledArg {
7132                label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
7133                arg: center_ast,
7134            },
7135        ],
7136        digest: None,
7137        non_code_meta: Default::default(),
7138    })))
7139}
7140
7141/// Create an AST node for horizontal(line)
7142pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
7143    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7144        callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
7145        unlabeled: Some(line_expr),
7146        arguments: Default::default(),
7147        digest: None,
7148        non_code_meta: Default::default(),
7149    })))
7150}
7151
7152/// Create an AST node for vertical(line)
7153pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
7154    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7155        callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
7156        unlabeled: Some(line_expr),
7157        arguments: Default::default(),
7158        digest: None,
7159        non_code_meta: Default::default(),
7160    })))
7161}
7162
7163/// Create a member expression like object.property (e.g., line1.end)
7164pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
7165    ast::Expr::MemberExpression(BoxNode::new(ast::Node::no_src(ast::MemberExpression {
7166        object: object_expr,
7167        property: ast::Expr::Name(BoxNode::new(ast::Node::no_src(ast::Name {
7168            name: ast::Node::no_src(ast::Identifier {
7169                name: property.to_string(),
7170                digest: None,
7171            }),
7172            path: Vec::new(),
7173            abs_path: false,
7174            digest: None,
7175        }))),
7176        computed: false,
7177        digest: None,
7178    })))
7179}
7180
7181pub(crate) fn create_index_expression(object_expr: ast::Expr, index: usize) -> ast::Expr {
7182    ast::Expr::MemberExpression(BoxNode::new(ast::Node::no_src(ast::MemberExpression {
7183        object: object_expr,
7184        property: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(
7185            ast::NumericLiteral {
7186                value: index as f64,
7187                suffix: NumericSuffix::None,
7188                raw: index.to_string(),
7189                digest: None,
7190            },
7191        )))),
7192        computed: true,
7193        digest: None,
7194    })))
7195}
7196
7197/// Create an AST node for `fixed([point, [x, y]])`.
7198fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
7199    // Create [x, y] array literal.
7200    let x_literal = ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
7201        position.x,
7202    )?))));
7203    let y_literal = ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
7204        position.y,
7205    )?))));
7206    let point_array = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7207        elements: vec![x_literal, y_literal],
7208        digest: None,
7209        non_code_meta: Default::default(),
7210    })));
7211
7212    // Create [point, [x, y]] outer array.
7213    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7214        elements: vec![point_expr, point_array],
7215        digest: None,
7216        non_code_meta: Default::default(),
7217    })));
7218
7219    // Create fixed([...])
7220    Ok(ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(
7221        ast::CallExpressionKw {
7222            callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
7223            unlabeled: Some(array_expr),
7224            arguments: Default::default(),
7225            digest: None,
7226            non_code_meta: Default::default(),
7227        },
7228    ))))
7229}
7230
7231/// Create an AST node for equalLength([line1, line2, ...])
7232pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
7233    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7234        elements: line_exprs,
7235        digest: None,
7236        non_code_meta: Default::default(),
7237    })));
7238
7239    // Create equalLength([...])
7240    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7241        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
7242        unlabeled: Some(array_expr),
7243        arguments: Default::default(),
7244        digest: None,
7245        non_code_meta: Default::default(),
7246    })))
7247}
7248
7249/// Create an AST node for equalRadius([seg1, seg2, ...])
7250pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
7251    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7252        elements: segment_exprs,
7253        digest: None,
7254        non_code_meta: Default::default(),
7255    })));
7256
7257    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7258        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
7259        unlabeled: Some(array_expr),
7260        arguments: Default::default(),
7261        digest: None,
7262        non_code_meta: Default::default(),
7263    })))
7264}
7265
7266/// Create an AST node for tangent([seg1, seg2])
7267pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
7268    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7269        elements: vec![seg1_expr, seg2_expr],
7270        digest: None,
7271        non_code_meta: Default::default(),
7272    })));
7273
7274    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7275        callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
7276        unlabeled: Some(array_expr),
7277        arguments: Default::default(),
7278        digest: None,
7279        non_code_meta: Default::default(),
7280    })))
7281}
7282
7283/// Create an AST node for symmetric([input1, input2], axis = line)
7284pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
7285    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7286        elements: input_exprs,
7287        digest: None,
7288        non_code_meta: Default::default(),
7289    })));
7290    let arguments = vec![ast::LabeledArg {
7291        label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
7292        arg: axis_expr,
7293    }];
7294
7295    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7296        callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
7297        unlabeled: Some(array_expr),
7298        arguments,
7299        digest: None,
7300        non_code_meta: Default::default(),
7301    })))
7302}
7303
7304/// Create an AST node for midpoint(segment, point = point)
7305pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
7306    let arguments = vec![ast::LabeledArg {
7307        label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
7308        arg: point_expr,
7309    }];
7310
7311    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7312        callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
7313        unlabeled: Some(segment_expr),
7314        arguments,
7315        digest: None,
7316        non_code_meta: Default::default(),
7317    })))
7318}
7319
7320/// The source range to attach to a diagnostic for `error` in the top-level
7321/// module. Source ranges are ordered innermost first and may point into
7322/// imported modules, so prefer the innermost range that is in the top-level
7323/// module; otherwise fall back to the innermost range.
7324fn issue_source_range(error: &KclError) -> SourceRange {
7325    let source_ranges = error.source_ranges();
7326    source_ranges
7327        .iter()
7328        .find(|range| range.is_top_level_module())
7329        .or_else(|| source_ranges.first())
7330        .copied()
7331        .unwrap_or_else(SourceRange::synthetic)
7332}
7333
7334#[cfg(test)]
7335mod tests {
7336    use std::sync;
7337
7338    use super::*;
7339    use crate::engine::PlaneName;
7340    use crate::engine::engine_manager::EngineManager;
7341    use crate::execution::cache::SketchModeState;
7342    use crate::execution::cache::clear_mem_cache;
7343    use crate::execution::cache::read_old_memory;
7344    use crate::execution::cache::write_old_memory;
7345    use crate::front::Distance;
7346    use crate::front::Fixed;
7347    use crate::front::FixedPoint;
7348    use crate::front::Midpoint;
7349    use crate::front::Object;
7350    use crate::front::Plane;
7351    use crate::front::Sketch;
7352    use crate::front::Tangent;
7353    use crate::frontend::sketch::Vertical;
7354    use crate::pretty::NumericSuffix;
7355
7356    fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
7357        for object in &scene_graph.objects {
7358            if let ObjectKind::Sketch(_) = &object.kind {
7359                return Some(object);
7360            }
7361        }
7362        None
7363    }
7364
7365    fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
7366        for object in &scene_graph.objects {
7367            if let ObjectKind::Face(_) = &object.kind {
7368                return Some(object);
7369            }
7370        }
7371        None
7372    }
7373
7374    fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
7375        for object in &scene_graph.objects {
7376            if matches!(&object.kind, ObjectKind::Wall(_)) {
7377                return Some(object.id);
7378            }
7379        }
7380        None
7381    }
7382
7383    fn find_cap_object_id_with_solid_output_index(
7384        scene_graph: &SceneGraph,
7385        cap_kind: crate::frontend::api::CapKind,
7386        solid_output_index: usize,
7387    ) -> Option<ObjectId> {
7388        for object in &scene_graph.objects {
7389            if matches!(&object.kind, ObjectKind::Cap(cap) if cap.kind == cap_kind && cap.solid_output_index == Some(solid_output_index))
7390            {
7391                return Some(object.id);
7392            }
7393        }
7394        None
7395    }
7396
7397    #[test]
7398    fn issue_source_range_prefers_top_level_module() {
7399        use kcl_error::ModuleId;
7400
7401        let top = SourceRange::new(10, 20, ModuleId::default());
7402        let imported = SourceRange::new(0, 5, ModuleId::from_usize(7));
7403
7404        // Innermost frame is in an imported module; the diagnostic should
7405        // land on the innermost top-level range instead.
7406        let error = KclError::new_semantic(crate::errors::KclErrorDetails::new(
7407            "boom".to_owned(),
7408            vec![imported, top],
7409        ));
7410        assert_eq!(super::issue_source_range(&error), top);
7411
7412        // No top-level range at all: fall back to the innermost one.
7413        let error = KclError::new_semantic(crate::errors::KclErrorDetails::new("boom".to_owned(), vec![imported]));
7414        assert_eq!(super::issue_source_range(&error), imported);
7415
7416        // No ranges: synthetic.
7417        let error = KclError::new_semantic(crate::errors::KclErrorDetails::new("boom".to_owned(), vec![]));
7418        assert_eq!(super::issue_source_range(&error), SourceRange::synthetic());
7419    }
7420
7421    #[test]
7422    fn composite_constituent_sweeps_are_not_solid_outputs() {
7423        use kcl_api::artifact::ArtifactSweepMethod;
7424        use kcl_api::artifact::CompositeSolid;
7425        use kcl_api::artifact::CompositeSolidSubType;
7426        use kcl_api::artifact::Sweep;
7427        use kcl_api::artifact::SweepSubType;
7428
7429        let first_sweep_id = ArtifactId::new(Uuid::new_v4());
7430        let second_sweep_id = ArtifactId::new(Uuid::new_v4());
7431        let composite_id = ArtifactId::new(Uuid::new_v4());
7432        let code_ref = CodeRef::placeholder(SourceRange::synthetic());
7433        let sweep = |id| {
7434            Artifact::Sweep(Sweep {
7435                id,
7436                sub_type: SweepSubType::Extrusion,
7437                path_id: ArtifactId::new(Uuid::new_v4()),
7438                surface_ids: Vec::new(),
7439                edge_ids: Vec::new(),
7440                code_ref: code_ref.clone(),
7441                source_sweep_id: None,
7442                trajectory_id: None,
7443                method: ArtifactSweepMethod::New,
7444                consumed: false,
7445                pattern_ids: Vec::new(),
7446            })
7447        };
7448        let mut artifacts = IndexMap::from([
7449            (first_sweep_id, sweep(first_sweep_id)),
7450            (second_sweep_id, sweep(second_sweep_id)),
7451        ]);
7452
7453        let top_level_graph = ArtifactGraph::from_parts(artifacts.clone(), artifacts.len());
7454        assert_eq!(
7455            solid_output_index_for_sweep(&top_level_graph, first_sweep_id, &code_ref),
7456            Some(0)
7457        );
7458        assert_eq!(
7459            solid_output_index_for_sweep(&top_level_graph, second_sweep_id, &code_ref),
7460            Some(1)
7461        );
7462
7463        artifacts.insert(
7464            composite_id,
7465            Artifact::CompositeSolid(CompositeSolid {
7466                id: composite_id,
7467                consumed: false,
7468                sub_type: CompositeSolidSubType::Union,
7469                output_index: None,
7470                solid_ids: vec![first_sweep_id, second_sweep_id],
7471                tool_ids: Vec::new(),
7472                code_ref,
7473                composite_solid_id: None,
7474                pattern_ids: Vec::new(),
7475            }),
7476        );
7477        let composite_graph = ArtifactGraph::from_parts(artifacts.clone(), artifacts.len());
7478        assert_eq!(
7479            solid_output_index_for_sweep(&composite_graph, first_sweep_id, &CodeRef::default()),
7480            None
7481        );
7482        assert_eq!(
7483            solid_output_index_for_sweep(&composite_graph, second_sweep_id, &CodeRef::default()),
7484            None
7485        );
7486    }
7487
7488    #[test]
7489    fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
7490        let source = "\
7491region001 = region(point = [0.1, 0.1], sketch = s)
7492extrude001 = extrude(region001, length = 5)
7493revolve001 = revolve(region001, axis = Y)
7494sweep001 = sweep(region001, path = path001)
7495loft001 = loft(region001)
7496not_sweep001 = shell(extrude001, faces = [], thickness = 1)
7497";
7498
7499        let program = Program::parse(source).unwrap().0.unwrap();
7500
7501        assert_eq!(
7502            region_name_from_sweep_variable(&program.ast, "extrude001"),
7503            Some("region001".to_owned())
7504        );
7505        assert_eq!(
7506            region_name_from_sweep_variable(&program.ast, "revolve001"),
7507            Some("region001".to_owned())
7508        );
7509        assert_eq!(
7510            region_name_from_sweep_variable(&program.ast, "sweep001"),
7511            Some("region001".to_owned())
7512        );
7513        assert_eq!(
7514            region_name_from_sweep_variable(&program.ast, "loft001"),
7515            Some("region001".to_owned())
7516        );
7517        assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
7518    }
7519
7520    #[track_caller]
7521    fn expect_sketch(object: &Object) -> &Sketch {
7522        if let ObjectKind::Sketch(sketch) = &object.kind {
7523            sketch
7524        } else {
7525            panic!("Object is not a sketch: {:?}", object);
7526        }
7527    }
7528
7529    fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
7530        let point_object = scene_graph.objects.get(point_id.0).unwrap();
7531        let ObjectKind::Segment {
7532            segment: Segment::Point(point),
7533        } = &point_object.kind
7534        else {
7535            panic!("Object is not a point segment: {point_object:?}");
7536        };
7537        point.position.clone()
7538    }
7539
7540    fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
7541        assert!((actual.x.value - expected.x.value).abs() < 1e-6);
7542        assert!((actual.y.value - expected.y.value).abs() < 1e-6);
7543    }
7544
7545    /// Build a millimeter-valued point expression for concise sketch edit test
7546    /// setup.
7547    fn point_expr_mm(x: f64, y: f64) -> Point2d<Expr> {
7548        Point2d {
7549            x: Expr::Var(Number {
7550                value: x,
7551                units: NumericSuffix::Mm,
7552            }),
7553            y: Expr::Var(Number {
7554                value: y,
7555                units: NumericSuffix::Mm,
7556            }),
7557        }
7558    }
7559
7560    /// Build a millimeter-valued numeric point for comparing solved scene graph
7561    /// positions.
7562    fn point_number_mm(x: f64, y: f64) -> Point2d<Number> {
7563        Point2d {
7564            x: Number {
7565                value: x,
7566                units: NumericSuffix::Mm,
7567            },
7568            y: Number {
7569                value: y,
7570                units: NumericSuffix::Mm,
7571            },
7572        }
7573    }
7574
7575    fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
7576        LineCtor {
7577            start: Point2d {
7578                x: Expr::Number(Number { value: start_x, units }),
7579                y: Expr::Number(Number { value: start_y, units }),
7580            },
7581            end: Point2d {
7582                x: Expr::Number(Number { value: end_x, units }),
7583                y: Expr::Number(Number { value: end_y, units }),
7584            },
7585            construction: None,
7586        }
7587    }
7588
7589    async fn create_sketch_with_single_line(
7590        frontend: &mut FrontendState,
7591        ctx: &ExecutorContext,
7592        mock_ctx: &ExecutorContext,
7593        version: Version,
7594    ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
7595        frontend.program = Program::empty();
7596
7597        let sketch_args = SketchCtor {
7598            on: Plane::Default(PlaneName::Xy),
7599        };
7600        let (_src_delta, _scene_delta, sketch_id) = frontend
7601            .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
7602            .await
7603            .unwrap();
7604
7605        let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
7606        let (source_delta, scene_graph_delta) = frontend
7607            .add_segment(mock_ctx, version, sketch_id, segment, None)
7608            .await
7609            .unwrap();
7610        let line_id = *scene_graph_delta
7611            .new_objects
7612            .last()
7613            .expect("Expected line object id to be created");
7614
7615        (sketch_id, line_id, source_delta, scene_graph_delta)
7616    }
7617
7618    async fn seed_frontend_with_mock(frontend: &mut FrontendState, mock_ctx: &ExecutorContext, program: &Program) {
7619        frontend.program = program.clone();
7620        let outcome = mock_ctx.run_mock(program, &MockConfig::default()).await.unwrap();
7621        frontend.update_state_after_exec(outcome, true);
7622    }
7623
7624    #[test]
7625    fn test_parse_frontend_mutation_source_error_messages_are_user_facing() {
7626        for (source, expected_message) in [
7627            ("**", "Error parsing KCL source after editing: Unexpected token: *"),
7628            ("3'", "Error parsing KCL source after editing: found unknown token '''"),
7629        ] {
7630            let err = parse_frontend_mutation_source(
7631                source,
7632                "Error parsing KCL source after editing",
7633                "No AST produced after editing",
7634            )
7635            .expect_err("expected invalid KCL source to fail");
7636            let message = err.error.message();
7637
7638            assert_eq!(message, expected_message);
7639            assert!(!message.contains("CompilationIssue"));
7640            assert!(!message.contains("KclErrorDetails"));
7641            assert!(!message.contains("source_range"));
7642        }
7643    }
7644
7645    #[tokio::test(flavor = "multi_thread")]
7646    async fn test_edit_constraint_value_parse_error_messages_are_user_facing() {
7647        let initial_source = "\
7648sketch(on = XY) {
7649  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7650  distance([line1.start, line1.end]) == 10
7651}
7652";
7653        let program = Program::parse(initial_source).unwrap().0.unwrap();
7654
7655        let mut frontend = FrontendState::new();
7656        let mock_ctx = ExecutorContext::new_mock(None).await;
7657        let version = Version(0);
7658
7659        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7660        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7661        let sketch_id = sketch_object.id;
7662        let sketch = expect_sketch(sketch_object);
7663        let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7664
7665        for (value, expected_message) in [
7666            ("**", "Invalid constraint value: Unexpected token: *"),
7667            ("3'", "Invalid constraint value: found unknown token '''"),
7668        ] {
7669            let err = frontend
7670                .edit_constraint_value(&mock_ctx, version, sketch_id, constraint_id, value.to_owned())
7671                .await
7672                .expect_err("expected invalid constraint expression to fail");
7673            let message = err.error.message();
7674
7675            assert_eq!(message, expected_message);
7676            assert!(!message.contains("CompilationIssue"));
7677            assert!(!message.contains("KclErrorDetails"));
7678            assert!(!message.contains("source_range"));
7679        }
7680
7681        mock_ctx.close().await;
7682    }
7683
7684    #[tokio::test(flavor = "multi_thread")]
7685    async fn test_failed_edit_constraint_value_does_not_update_program() {
7686        let initial_source = "\
7687sketch(on = XY) {
7688  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7689  distance([line1.start, line1.end]) == 10
7690}
7691";
7692        let program = Program::parse(initial_source).unwrap().0.unwrap();
7693        let original_source = program.original_file_contents.clone();
7694
7695        let mut frontend = FrontendState::new();
7696        let mock_ctx = ExecutorContext::new_mock(None).await;
7697        let version = Version(0);
7698
7699        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7700        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7701        let sketch_id = sketch_object.id;
7702        let sketch = expect_sketch(sketch_object);
7703        let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7704
7705        frontend
7706            .edit_constraint_value(
7707                &mock_ctx,
7708                version,
7709                sketch_id,
7710                constraint_id,
7711                "unknownDistance".to_owned(),
7712            )
7713            .await
7714            .expect_err("expected invalid constraint value to fail execution");
7715
7716        assert_eq!(frontend.program.original_file_contents, original_source);
7717        assert!(!frontend.program.original_file_contents.contains("unknownDistance"));
7718
7719        mock_ctx.close().await;
7720    }
7721
7722    #[tokio::test(flavor = "multi_thread")]
7723    async fn test_edit_constraint_value_array_index_oob_fails_in_sketch_mode() {
7724        let initial_source = "\
7725arr = [0]
7726sketch(on = XY) {
7727  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7728  distance([line1.start, line1.end]) == 10
7729}
7730";
7731        let program = Program::parse(initial_source).unwrap().0.unwrap();
7732
7733        let mut frontend = FrontendState::new();
7734        let mock_ctx = ExecutorContext::new_mock(None).await;
7735        let version = Version(0);
7736
7737        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7738        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7739        let sketch_id = sketch_object.id;
7740        let sketch = expect_sketch(sketch_object);
7741        let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7742
7743        // In sketch mode execution, an out-of-bounds array index is an error.
7744        // It should not fall back to the first element of the array the way
7745        // plain mock execution does.
7746        let err = frontend
7747            .edit_constraint_value(&mock_ctx, version, sketch_id, constraint_id, "arr[5]".to_owned())
7748            .await
7749            .expect_err("expected out-of-bounds array index to be an error in sketch mode execution");
7750        let message = err.error.message();
7751        assert!(
7752            message.contains("The array doesn't have any item at index 5"),
7753            "unexpected error message: {message}"
7754        );
7755
7756        mock_ctx.close().await;
7757    }
7758
7759    #[tokio::test(flavor = "multi_thread")]
7760    async fn test_sketch_checkpoint_round_trip_restores_state() {
7761        let mut frontend = FrontendState::new();
7762        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7763        let mock_ctx = ExecutorContext::new_mock(None).await;
7764        let version = Version(0);
7765
7766        let (sketch_id, line_id, source_delta, scene_graph_delta) =
7767            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7768
7769        let expected_source = source_delta.text.clone();
7770        let expected_scene_graph = frontend.scene_graph.clone();
7771        let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
7772        let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
7773
7774        let checkpoint_id = frontend
7775            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7776            .await
7777            .unwrap();
7778
7779        let edited_segments = vec![ExistingSegmentCtor {
7780            id: line_id,
7781            ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
7782        }];
7783        let (edited_source, _edited_scene) = frontend
7784            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
7785            .await
7786            .unwrap();
7787        assert_ne!(edited_source.text, expected_source);
7788
7789        let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
7790
7791        assert_eq!(restored.source_delta.text, expected_source);
7792        assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
7793        assert!(restored.scene_graph_delta.invalidates_ids);
7794        assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
7795        assert_eq!(frontend.scene_graph, expected_scene_graph);
7796        assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
7797
7798        ctx.close().await;
7799    }
7800
7801    #[tokio::test(flavor = "multi_thread")]
7802    async fn test_sketch_checkpoints_prune_oldest_entries() {
7803        let mut frontend = FrontendState::new();
7804        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7805        let mock_ctx = ExecutorContext::new_mock(None).await;
7806        let version = Version(0);
7807
7808        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7809            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7810
7811        let mut checkpoint_ids = Vec::new();
7812        for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
7813            checkpoint_ids.push(
7814                frontend
7815                    .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7816                    .await
7817                    .unwrap(),
7818            );
7819        }
7820
7821        assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
7822        assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
7823
7824        let oldest_retained = checkpoint_ids[3];
7825        assert_eq!(
7826            frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
7827            Some(oldest_retained)
7828        );
7829
7830        let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
7831        assert!(evicted_restore.is_err());
7832        assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
7833
7834        frontend
7835            .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
7836            .await
7837            .unwrap();
7838
7839        ctx.close().await;
7840    }
7841
7842    #[tokio::test(flavor = "multi_thread")]
7843    async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
7844        let mut frontend = FrontendState::new();
7845        let missing_checkpoint = SketchCheckpointId::new(999);
7846
7847        let err = frontend
7848            .restore_sketch_checkpoint(missing_checkpoint)
7849            .await
7850            .expect_err("Expected restore to fail for missing checkpoint");
7851
7852        assert!(err.msg.contains("Sketch checkpoint not found"));
7853    }
7854
7855    #[tokio::test(flavor = "multi_thread")]
7856    async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
7857        let mut frontend = FrontendState::new();
7858        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7859        let mock_ctx = ExecutorContext::new_mock(None).await;
7860        let version = Version(0);
7861
7862        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7863            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7864
7865        let checkpoint_a = frontend
7866            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7867            .await
7868            .unwrap();
7869        let checkpoint_b = frontend
7870            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7871            .await
7872            .unwrap();
7873        assert_eq!(frontend.sketch_checkpoints.len(), 2);
7874
7875        frontend.clear_sketch_checkpoints();
7876        assert!(frontend.sketch_checkpoints.is_empty());
7877        frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
7878        frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
7879
7880        ctx.close().await;
7881    }
7882
7883    #[tokio::test(flavor = "multi_thread")]
7884    async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
7885        let mut frontend = FrontendState::new();
7886        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7887        let mock_ctx = ExecutorContext::new_mock(None).await;
7888        let version = Version(0);
7889
7890        let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
7891            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7892        let old_source = source_delta.text.clone();
7893        let old_checkpoint = frontend
7894            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7895            .await
7896            .unwrap();
7897        let initial_checkpoint_count = frontend.sketch_checkpoints.len();
7898
7899        let new_program = Program::parse("sketch(on = XY) {\n  point(at = [1mm, 2mm])\n}\n")
7900            .unwrap()
7901            .0
7902            .unwrap();
7903
7904        let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
7905        let SetProgramOutcome::Success {
7906            checkpoint_id: Some(new_checkpoint),
7907            ..
7908        } = result
7909        else {
7910            panic!("Expected Success with a fresh checkpoint baseline");
7911        };
7912
7913        assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
7914
7915        let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7916        assert_eq!(old_restore.source_delta.text, old_source);
7917
7918        let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
7919        assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
7920
7921        ctx.close().await;
7922    }
7923
7924    #[tokio::test(flavor = "multi_thread")]
7925    async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
7926        let mut frontend = FrontendState::new();
7927        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7928        let mock_ctx = ExecutorContext::new_mock(None).await;
7929        let version = Version(0);
7930
7931        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7932            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7933        let old_checkpoint = frontend
7934            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7935            .await
7936            .unwrap();
7937        let checkpoint_count_before = frontend.sketch_checkpoints.len();
7938
7939        let failing_program = Program::parse(
7940            "sketch(on = XY) {\n  line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
7941        )
7942        .unwrap()
7943        .0
7944        .unwrap();
7945
7946        let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
7947        assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
7948        assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
7949        frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7950
7951        ctx.close().await;
7952    }
7953
7954    #[tokio::test(flavor = "multi_thread")]
7955    async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
7956        let mut frontend = FrontendState::new();
7957        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7958
7959        let program = Program::parse(
7960            "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",
7961        )
7962        .unwrap()
7963        .0
7964        .unwrap();
7965        let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
7966        let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
7967            panic!("Expected successful baseline program execution");
7968        };
7969
7970        clear_mem_cache().await;
7971        assert!(read_old_memory().await.is_none());
7972
7973        let checkpoint_without_mock_memory = frontend
7974            .create_sketch_checkpoint((*exec_outcome).clone())
7975            .await
7976            .unwrap();
7977
7978        write_old_memory(SketchModeState::new_for_tests()).await;
7979        assert!(read_old_memory().await.is_some());
7980
7981        let checkpoint_with_mock_memory = frontend
7982            .create_sketch_checkpoint((*exec_outcome).clone())
7983            .await
7984            .unwrap();
7985
7986        clear_mem_cache().await;
7987        assert!(read_old_memory().await.is_none());
7988
7989        frontend
7990            .restore_sketch_checkpoint(checkpoint_with_mock_memory)
7991            .await
7992            .unwrap();
7993        assert!(read_old_memory().await.is_some());
7994
7995        frontend
7996            .restore_sketch_checkpoint(checkpoint_without_mock_memory)
7997            .await
7998            .unwrap();
7999        assert!(read_old_memory().await.is_none());
8000
8001        ctx.close().await;
8002    }
8003
8004    #[tokio::test(flavor = "multi_thread")]
8005    async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
8006        let source = "\
8007sketch(on = XY) {
8008  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
8009}
8010
8011bad = missing_name
8012";
8013        let program = Program::parse(source).unwrap().0.unwrap();
8014
8015        let mut frontend = FrontendState::new();
8016
8017        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8018        let mock_ctx = ExecutorContext::new_mock(None).await;
8019        let version = Version(0);
8020        let project_id = ProjectId(0);
8021        let file_id = FileId(0);
8022
8023        let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
8024            panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
8025        };
8026
8027        let sketch_id = frontend
8028            .scene_graph
8029            .objects
8030            .iter()
8031            .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
8032            .expect("Expected sketch object from errored hack_set_program");
8033
8034        frontend
8035            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
8036            .await
8037            .unwrap();
8038
8039        ctx.close().await;
8040        mock_ctx.close().await;
8041    }
8042
8043    #[tokio::test(flavor = "multi_thread")]
8044    async fn test_new_sketch_add_point_edit_point() {
8045        let program = Program::empty();
8046
8047        let mut frontend = FrontendState::new();
8048        frontend.program = program;
8049
8050        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8051        let mock_ctx = ExecutorContext::new_mock(None).await;
8052        let version = Version(0);
8053
8054        let sketch_args = SketchCtor {
8055            on: Plane::Default(PlaneName::Xy),
8056        };
8057        let (_src_delta, scene_delta, sketch_id) = frontend
8058            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8059            .await
8060            .unwrap();
8061        assert_eq!(sketch_id, ObjectId(1));
8062        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8063        let sketch_object = &scene_delta.new_graph.objects[1];
8064        assert_eq!(sketch_object.id, ObjectId(1));
8065        assert_eq!(
8066            sketch_object.kind,
8067            ObjectKind::Sketch(Sketch {
8068                args: SketchCtor {
8069                    on: Plane::Default(PlaneName::Xy)
8070                },
8071                plane: ObjectId(0),
8072                segments: vec![],
8073                constraints: vec![],
8074            })
8075        );
8076        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8077
8078        let point_ctor = PointCtor {
8079            position: Point2d {
8080                x: Expr::Number(Number {
8081                    value: 1.0,
8082                    units: NumericSuffix::Inch,
8083                }),
8084                y: Expr::Number(Number {
8085                    value: 2.0,
8086                    units: NumericSuffix::Inch,
8087                }),
8088            },
8089        };
8090        let segment = SegmentCtor::Point(point_ctor);
8091        let (src_delta, scene_delta) = frontend
8092            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8093            .await
8094            .unwrap();
8095        insta::assert_snapshot!("test_new_sketch_add_point_edit_point_1", src_delta.text.as_str());
8096        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
8097        assert_eq!(scene_delta.new_graph.objects.len(), 3);
8098        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8099            assert_eq!(scene_object.id.0, i);
8100        }
8101
8102        let point_id = *scene_delta.new_objects.last().unwrap();
8103
8104        let point_ctor = PointCtor {
8105            position: Point2d {
8106                x: Expr::Number(Number {
8107                    value: 3.0,
8108                    units: NumericSuffix::Inch,
8109                }),
8110                y: Expr::Number(Number {
8111                    value: 4.0,
8112                    units: NumericSuffix::Inch,
8113                }),
8114            },
8115        };
8116        let segments = vec![ExistingSegmentCtor {
8117            id: point_id,
8118            ctor: SegmentCtor::Point(point_ctor),
8119        }];
8120        let (src_delta, scene_delta) = frontend
8121            .edit_segments(&mock_ctx, version, sketch_id, segments)
8122            .await
8123            .unwrap();
8124        insta::assert_snapshot!("test_new_sketch_add_point_edit_point_2", src_delta.text.as_str());
8125        assert_eq!(scene_delta.new_objects, vec![]);
8126        assert_eq!(scene_delta.new_graph.objects.len(), 3);
8127
8128        ctx.close().await;
8129        mock_ctx.close().await;
8130    }
8131
8132    #[tokio::test(flavor = "multi_thread")]
8133    async fn test_new_sketch_add_line_edit_line() {
8134        let program = Program::empty();
8135
8136        let mut frontend = FrontendState::new();
8137        frontend.program = program;
8138
8139        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8140        let mock_ctx = ExecutorContext::new_mock(None).await;
8141        let version = Version(0);
8142
8143        let sketch_args = SketchCtor {
8144            on: Plane::Default(PlaneName::Xy),
8145        };
8146        let (_src_delta, scene_delta, sketch_id) = frontend
8147            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8148            .await
8149            .unwrap();
8150        assert_eq!(sketch_id, ObjectId(1));
8151        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8152        let sketch_object = &scene_delta.new_graph.objects[1];
8153        assert_eq!(sketch_object.id, ObjectId(1));
8154        assert_eq!(
8155            sketch_object.kind,
8156            ObjectKind::Sketch(Sketch {
8157                args: SketchCtor {
8158                    on: Plane::Default(PlaneName::Xy)
8159                },
8160                plane: ObjectId(0),
8161                segments: vec![],
8162                constraints: vec![],
8163            })
8164        );
8165        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8166
8167        let line_ctor = LineCtor {
8168            start: Point2d {
8169                x: Expr::Number(Number {
8170                    value: 0.0,
8171                    units: NumericSuffix::Mm,
8172                }),
8173                y: Expr::Number(Number {
8174                    value: 0.0,
8175                    units: NumericSuffix::Mm,
8176                }),
8177            },
8178            end: Point2d {
8179                x: Expr::Number(Number {
8180                    value: 10.0,
8181                    units: NumericSuffix::Mm,
8182                }),
8183                y: Expr::Number(Number {
8184                    value: 10.0,
8185                    units: NumericSuffix::Mm,
8186                }),
8187            },
8188            construction: None,
8189        };
8190        let segment = SegmentCtor::Line(line_ctor);
8191        let (src_delta, scene_delta) = frontend
8192            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8193            .await
8194            .unwrap();
8195        insta::assert_snapshot!("test_new_sketch_add_line_edit_line_1", src_delta.text.as_str());
8196        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8197        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8198        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8199            assert_eq!(scene_object.id.0, i);
8200        }
8201
8202        // The new objects are the end points and then the line.
8203        let line = *scene_delta.new_objects.last().unwrap();
8204
8205        let line_ctor = LineCtor {
8206            start: Point2d {
8207                x: Expr::Number(Number {
8208                    value: 1.0,
8209                    units: NumericSuffix::Mm,
8210                }),
8211                y: Expr::Number(Number {
8212                    value: 2.0,
8213                    units: NumericSuffix::Mm,
8214                }),
8215            },
8216            end: Point2d {
8217                x: Expr::Number(Number {
8218                    value: 13.0,
8219                    units: NumericSuffix::Mm,
8220                }),
8221                y: Expr::Number(Number {
8222                    value: 14.0,
8223                    units: NumericSuffix::Mm,
8224                }),
8225            },
8226            construction: None,
8227        };
8228        let segments = vec![ExistingSegmentCtor {
8229            id: line,
8230            ctor: SegmentCtor::Line(line_ctor),
8231        }];
8232        let (src_delta, scene_delta) = frontend
8233            .edit_segments(&mock_ctx, version, sketch_id, segments)
8234            .await
8235            .unwrap();
8236        insta::assert_snapshot!("test_new_sketch_add_line_edit_line_2", src_delta.text.as_str());
8237        assert_eq!(scene_delta.new_objects, vec![]);
8238        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8239
8240        ctx.close().await;
8241        mock_ctx.close().await;
8242    }
8243
8244    #[tokio::test(flavor = "multi_thread")]
8245    async fn test_new_sketch_add_arc_edit_arc() {
8246        let program = Program::empty();
8247
8248        let mut frontend = FrontendState::new();
8249        frontend.program = program;
8250
8251        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8252        let mock_ctx = ExecutorContext::new_mock(None).await;
8253        let version = Version(0);
8254
8255        let sketch_args = SketchCtor {
8256            on: Plane::Default(PlaneName::Xy),
8257        };
8258        let (_src_delta, scene_delta, sketch_id) = frontend
8259            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8260            .await
8261            .unwrap();
8262        assert_eq!(sketch_id, ObjectId(1));
8263        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8264        let sketch_object = &scene_delta.new_graph.objects[1];
8265        assert_eq!(sketch_object.id, ObjectId(1));
8266        assert_eq!(
8267            sketch_object.kind,
8268            ObjectKind::Sketch(Sketch {
8269                args: SketchCtor {
8270                    on: Plane::Default(PlaneName::Xy),
8271                },
8272                plane: ObjectId(0),
8273                segments: vec![],
8274                constraints: vec![],
8275            })
8276        );
8277        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8278
8279        let arc_ctor = ArcCtor {
8280            start: Point2d {
8281                x: Expr::Var(Number {
8282                    value: 0.0,
8283                    units: NumericSuffix::Mm,
8284                }),
8285                y: Expr::Var(Number {
8286                    value: 0.0,
8287                    units: NumericSuffix::Mm,
8288                }),
8289            },
8290            end: Point2d {
8291                x: Expr::Var(Number {
8292                    value: 10.0,
8293                    units: NumericSuffix::Mm,
8294                }),
8295                y: Expr::Var(Number {
8296                    value: 10.0,
8297                    units: NumericSuffix::Mm,
8298                }),
8299            },
8300            center: Point2d {
8301                x: Expr::Var(Number {
8302                    value: 10.0,
8303                    units: NumericSuffix::Mm,
8304                }),
8305                y: Expr::Var(Number {
8306                    value: 0.0,
8307                    units: NumericSuffix::Mm,
8308                }),
8309            },
8310            direction: None,
8311            construction: None,
8312        };
8313        let segment = SegmentCtor::Arc(arc_ctor);
8314        let (src_delta, scene_delta) = frontend
8315            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8316            .await
8317            .unwrap();
8318        insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_1", src_delta.text.as_str());
8319        assert_eq!(
8320            scene_delta.new_objects,
8321            vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
8322        );
8323        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8324            assert_eq!(scene_object.id.0, i);
8325        }
8326        assert_eq!(scene_delta.new_graph.objects.len(), 6);
8327
8328        // The new objects are the end points, the center, and then the arc.
8329        let arc = *scene_delta.new_objects.last().unwrap();
8330
8331        let arc_ctor = ArcCtor {
8332            start: Point2d {
8333                x: Expr::Var(Number {
8334                    value: 1.0,
8335                    units: NumericSuffix::Mm,
8336                }),
8337                y: Expr::Var(Number {
8338                    value: 2.0,
8339                    units: NumericSuffix::Mm,
8340                }),
8341            },
8342            end: Point2d {
8343                x: Expr::Var(Number {
8344                    value: 13.0,
8345                    units: NumericSuffix::Mm,
8346                }),
8347                y: Expr::Var(Number {
8348                    value: 14.0,
8349                    units: NumericSuffix::Mm,
8350                }),
8351            },
8352            center: Point2d {
8353                x: Expr::Var(Number {
8354                    value: 13.0,
8355                    units: NumericSuffix::Mm,
8356                }),
8357                y: Expr::Var(Number {
8358                    value: 2.0,
8359                    units: NumericSuffix::Mm,
8360                }),
8361            },
8362            direction: None,
8363            construction: None,
8364        };
8365        let segments = vec![ExistingSegmentCtor {
8366            id: arc,
8367            ctor: SegmentCtor::Arc(arc_ctor),
8368        }];
8369        let (src_delta, scene_delta) = frontend
8370            .edit_segments(&mock_ctx, version, sketch_id, segments)
8371            .await
8372            .unwrap();
8373        insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_2", src_delta.text.as_str());
8374        assert_eq!(scene_delta.new_objects, vec![]);
8375        assert_eq!(scene_delta.new_graph.objects.len(), 6);
8376
8377        ctx.close().await;
8378        mock_ctx.close().await;
8379    }
8380
8381    #[tokio::test(flavor = "multi_thread")]
8382    async fn test_new_sketch_add_circle_edit_circle() {
8383        let program = Program::empty();
8384
8385        let mut frontend = FrontendState::new();
8386        frontend.program = program;
8387
8388        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8389        let mock_ctx = ExecutorContext::new_mock(None).await;
8390        let version = Version(0);
8391
8392        let sketch_args = SketchCtor {
8393            on: Plane::Default(PlaneName::Xy),
8394        };
8395        let (_src_delta, _scene_delta, sketch_id) = frontend
8396            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8397            .await
8398            .unwrap();
8399
8400        // Add a circle segment.
8401        let circle_ctor = CircleCtor {
8402            start: Point2d {
8403                x: Expr::Var(Number {
8404                    value: 5.0,
8405                    units: NumericSuffix::Mm,
8406                }),
8407                y: Expr::Var(Number {
8408                    value: 0.0,
8409                    units: NumericSuffix::Mm,
8410                }),
8411            },
8412            center: Point2d {
8413                x: Expr::Var(Number {
8414                    value: 0.0,
8415                    units: NumericSuffix::Mm,
8416                }),
8417                y: Expr::Var(Number {
8418                    value: 0.0,
8419                    units: NumericSuffix::Mm,
8420                }),
8421            },
8422            construction: None,
8423        };
8424        let segment = SegmentCtor::Circle(circle_ctor);
8425        let (src_delta, scene_delta) = frontend
8426            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8427            .await
8428            .unwrap();
8429        insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_1", src_delta.text.as_str());
8430        // The new objects are start, center, and then the circle segment.
8431        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8432        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8433
8434        let circle = *scene_delta.new_objects.last().unwrap();
8435
8436        // Edit the circle segment.
8437        let circle_ctor = CircleCtor {
8438            start: Point2d {
8439                x: Expr::Var(Number {
8440                    value: 10.0,
8441                    units: NumericSuffix::Mm,
8442                }),
8443                y: Expr::Var(Number {
8444                    value: 0.0,
8445                    units: NumericSuffix::Mm,
8446                }),
8447            },
8448            center: Point2d {
8449                x: Expr::Var(Number {
8450                    value: 3.0,
8451                    units: NumericSuffix::Mm,
8452                }),
8453                y: Expr::Var(Number {
8454                    value: 4.0,
8455                    units: NumericSuffix::Mm,
8456                }),
8457            },
8458            construction: None,
8459        };
8460        let segments = vec![ExistingSegmentCtor {
8461            id: circle,
8462            ctor: SegmentCtor::Circle(circle_ctor),
8463        }];
8464        let (src_delta, scene_delta) = frontend
8465            .edit_segments(&mock_ctx, version, sketch_id, segments)
8466            .await
8467            .unwrap();
8468        insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_2", src_delta.text.as_str());
8469        assert_eq!(scene_delta.new_objects, vec![]);
8470        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8471
8472        ctx.close().await;
8473        mock_ctx.close().await;
8474    }
8475
8476    #[tokio::test(flavor = "multi_thread")]
8477    async fn test_delete_circle() {
8478        let initial_source = "sketch001 = sketch(on = XY) {
8479  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8480}
8481";
8482
8483        let program = Program::parse(initial_source).unwrap().0.unwrap();
8484        let mut frontend = FrontendState::new();
8485
8486        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8487        let mock_ctx = ExecutorContext::new_mock(None).await;
8488        let version = Version(0);
8489
8490        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8491        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8492        let sketch_id = sketch_object.id;
8493        let sketch = expect_sketch(sketch_object);
8494
8495        // The sketch should have 3 segments: start point, center point, and the circle.
8496        assert_eq!(sketch.segments.len(), 3);
8497        let circle_id = sketch.segments[2];
8498
8499        // Delete the circle.
8500        let (src_delta, scene_delta) = frontend
8501            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
8502            .await
8503            .unwrap();
8504        insta::assert_snapshot!("test_delete_circle", src_delta.text.as_str());
8505        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8506        let new_sketch = expect_sketch(new_sketch_object);
8507        assert_eq!(new_sketch.segments.len(), 0);
8508
8509        ctx.close().await;
8510        mock_ctx.close().await;
8511    }
8512
8513    #[tokio::test(flavor = "multi_thread")]
8514    async fn test_edit_circle_via_point() {
8515        let initial_source = "sketch001 = sketch(on = XY) {
8516  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8517}
8518";
8519
8520        let program = Program::parse(initial_source).unwrap().0.unwrap();
8521        let mut frontend = FrontendState::new();
8522
8523        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8524        let mock_ctx = ExecutorContext::new_mock(None).await;
8525        let version = Version(0);
8526
8527        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8528        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8529        let sketch_id = sketch_object.id;
8530        let sketch = expect_sketch(sketch_object);
8531
8532        // Find the circle segment and its start point.
8533        let circle_id = sketch
8534            .segments
8535            .iter()
8536            .copied()
8537            .find(|seg_id| {
8538                matches!(
8539                    &frontend.scene_graph.objects[seg_id.0].kind,
8540                    ObjectKind::Segment {
8541                        segment: Segment::Circle(_)
8542                    }
8543                )
8544            })
8545            .expect("Expected a circle segment in sketch");
8546        let circle_object = &frontend.scene_graph.objects[circle_id.0];
8547        let ObjectKind::Segment {
8548            segment: Segment::Circle(circle),
8549        } = &circle_object.kind
8550        else {
8551            panic!("Expected circle segment, got: {:?}", circle_object.kind);
8552        };
8553        let start_point_id = circle.start;
8554
8555        // Edit the start point via SegmentCtor::Point.
8556        let segments = vec![ExistingSegmentCtor {
8557            id: start_point_id,
8558            ctor: SegmentCtor::Point(PointCtor {
8559                position: Point2d {
8560                    x: Expr::Var(Number {
8561                        value: 7.0,
8562                        units: NumericSuffix::Mm,
8563                    }),
8564                    y: Expr::Var(Number {
8565                        value: 1.0,
8566                        units: NumericSuffix::Mm,
8567                    }),
8568                },
8569            }),
8570        }];
8571        let (src_delta, _scene_delta) = frontend
8572            .edit_segments(&mock_ctx, version, sketch_id, segments)
8573            .await
8574            .unwrap();
8575        insta::assert_snapshot!("test_edit_circle_via_point", src_delta.text.as_str());
8576
8577        ctx.close().await;
8578        mock_ctx.close().await;
8579    }
8580
8581    #[tokio::test(flavor = "multi_thread")]
8582    async fn test_add_line_when_sketch_block_uses_variable() {
8583        let initial_source = "s = sketch(on = XY) {}
8584";
8585
8586        let program = Program::parse(initial_source).unwrap().0.unwrap();
8587
8588        let mut frontend = FrontendState::new();
8589
8590        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8591        let mock_ctx = ExecutorContext::new_mock(None).await;
8592        let version = Version(0);
8593
8594        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8595        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8596        let sketch_id = sketch_object.id;
8597
8598        let line_ctor = LineCtor {
8599            start: Point2d {
8600                x: Expr::Number(Number {
8601                    value: 0.0,
8602                    units: NumericSuffix::Mm,
8603                }),
8604                y: Expr::Number(Number {
8605                    value: 0.0,
8606                    units: NumericSuffix::Mm,
8607                }),
8608            },
8609            end: Point2d {
8610                x: Expr::Number(Number {
8611                    value: 10.0,
8612                    units: NumericSuffix::Mm,
8613                }),
8614                y: Expr::Number(Number {
8615                    value: 10.0,
8616                    units: NumericSuffix::Mm,
8617                }),
8618            },
8619            construction: None,
8620        };
8621        let segment = SegmentCtor::Line(line_ctor);
8622        let (src_delta, scene_delta) = frontend
8623            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8624            .await
8625            .unwrap();
8626        insta::assert_snapshot!("test_add_line_when_sketch_block_uses_variable", src_delta.text.as_str());
8627        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8628        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8629
8630        ctx.close().await;
8631        mock_ctx.close().await;
8632    }
8633
8634    #[tokio::test(flavor = "multi_thread")]
8635    async fn test_new_sketch_add_line_delete_sketch() {
8636        let program = Program::empty();
8637
8638        let mut frontend = FrontendState::new();
8639        frontend.program = program;
8640
8641        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8642        let mock_ctx = ExecutorContext::new_mock(None).await;
8643        let version = Version(0);
8644
8645        let sketch_args = SketchCtor {
8646            on: Plane::Default(PlaneName::Xy),
8647        };
8648        let (_src_delta, scene_delta, sketch_id) = frontend
8649            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8650            .await
8651            .unwrap();
8652        assert_eq!(sketch_id, ObjectId(1));
8653        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8654        let sketch_object = &scene_delta.new_graph.objects[1];
8655        assert_eq!(sketch_object.id, ObjectId(1));
8656        assert_eq!(
8657            sketch_object.kind,
8658            ObjectKind::Sketch(Sketch {
8659                args: SketchCtor {
8660                    on: Plane::Default(PlaneName::Xy)
8661                },
8662                plane: ObjectId(0),
8663                segments: vec![],
8664                constraints: vec![],
8665            })
8666        );
8667        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8668
8669        let line_ctor = LineCtor {
8670            start: Point2d {
8671                x: Expr::Number(Number {
8672                    value: 0.0,
8673                    units: NumericSuffix::Mm,
8674                }),
8675                y: Expr::Number(Number {
8676                    value: 0.0,
8677                    units: NumericSuffix::Mm,
8678                }),
8679            },
8680            end: Point2d {
8681                x: Expr::Number(Number {
8682                    value: 10.0,
8683                    units: NumericSuffix::Mm,
8684                }),
8685                y: Expr::Number(Number {
8686                    value: 10.0,
8687                    units: NumericSuffix::Mm,
8688                }),
8689            },
8690            construction: None,
8691        };
8692        let segment = SegmentCtor::Line(line_ctor);
8693        let (src_delta, scene_delta) = frontend
8694            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8695            .await
8696            .unwrap();
8697        insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_1", src_delta.text.as_str());
8698        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8699
8700        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8701        insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_2", src_delta.text.as_str());
8702        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8703
8704        ctx.close().await;
8705        mock_ctx.close().await;
8706    }
8707
8708    #[tokio::test(flavor = "multi_thread")]
8709    async fn test_delete_sketch_when_sketch_block_uses_variable() {
8710        let initial_source = "s = sketch(on = XY) {}
8711";
8712
8713        let program = Program::parse(initial_source).unwrap().0.unwrap();
8714
8715        let mut frontend = FrontendState::new();
8716
8717        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8718        let version = Version(0);
8719
8720        frontend.hack_set_program(&ctx, program).await.unwrap();
8721        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8722        let sketch_id = sketch_object.id;
8723
8724        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8725        insta::assert_snapshot!(
8726            "test_delete_sketch_when_sketch_block_uses_variable",
8727            src_delta.text.as_str()
8728        );
8729        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8730
8731        ctx.close().await;
8732    }
8733
8734    #[tokio::test(flavor = "multi_thread")]
8735    async fn test_delete_sketch_after_comment() {
8736        let initial_source = "sketch001 = sketch(on = XZ) {
8737}
8738";
8739
8740        let program = Program::parse(initial_source).unwrap().0.unwrap();
8741        let mut frontend = FrontendState::new();
8742
8743        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8744        let version = Version(0);
8745
8746        frontend.hack_set_program(&ctx, program).await.unwrap();
8747        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8748        let sketch_id = sketch_object.id;
8749        let original_source = sketch_object.source.clone();
8750
8751        let commented_source = "// test 1
8752sketch001 = sketch(on = XZ) {
8753}
8754";
8755        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8756        frontend.engine_execute(&ctx, commented_program).await.unwrap();
8757
8758        let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
8759        assert_eq!(cached_sketch_object.source, original_source);
8760
8761        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8762        assert!(
8763            !src_delta.text.contains("sketch001"),
8764            "sketch was not deleted: {}",
8765            src_delta.text
8766        );
8767        // The leading line comment must survive deletion.
8768        insta::assert_snapshot!("test_delete_sketch_after_comment", src_delta.text.as_str());
8769        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8770
8771        ctx.close().await;
8772    }
8773
8774    #[tokio::test(flavor = "multi_thread")]
8775    async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
8776        let initial_source = "sketch001 = sketch(on = XZ) {
8777}
8778foo = 1
8779";
8780
8781        let program = Program::parse(initial_source).unwrap().0.unwrap();
8782        let mut frontend = FrontendState::new();
8783
8784        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8785        let version = Version(0);
8786
8787        frontend.hack_set_program(&ctx, program).await.unwrap();
8788        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8789        let sketch_id = sketch_object.id;
8790
8791        let commented_source = "// keep me
8792sketch001 = sketch(on = XZ) {
8793}
8794foo = 1
8795";
8796        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8797        frontend.engine_execute(&ctx, commented_program).await.unwrap();
8798
8799        let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8800        // The leading comment should remain, now attached to the following body item.
8801        insta::assert_snapshot!(
8802            "test_delete_sketch_preserves_pre_comment_when_followed_by_code",
8803            src_delta.text.as_str()
8804        );
8805
8806        ctx.close().await;
8807    }
8808
8809    #[tokio::test(flavor = "multi_thread")]
8810    async fn test_delete_segment_preserves_pre_comment() {
8811        let initial_source = "\
8812sketch(on = XY) {
8813  point(at = [var 1, var 2])
8814  // describe the middle point
8815  point(at = [var 3, var 4])
8816  point(at = [var 5, var 6])
8817}
8818";
8819
8820        let program = Program::parse(initial_source).unwrap().0.unwrap();
8821        let mut frontend = FrontendState::new();
8822
8823        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8824        let mock_ctx = ExecutorContext::new_mock(None).await;
8825        let version = Version(0);
8826
8827        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8828        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8829        let sketch_id = sketch_object.id;
8830        let sketch = expect_sketch(sketch_object);
8831
8832        let middle_point_id = *sketch.segments.get(1).unwrap();
8833
8834        let (src_delta, _scene_delta) = frontend
8835            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8836            .await
8837            .unwrap();
8838        // The line comment on the line above the deleted point must be preserved.
8839        // It is reattached to the next surviving body item.
8840        insta::assert_snapshot!("test_delete_segment_preserves_pre_comment", src_delta.text.as_str());
8841
8842        ctx.close().await;
8843        mock_ctx.close().await;
8844    }
8845
8846    #[tokio::test(flavor = "multi_thread")]
8847    async fn test_delete_last_segment_preserves_pre_comment() {
8848        let initial_source = "\
8849sketch(on = XY) {
8850  point(at = [var 1, var 2])
8851  // describe the trailing point
8852  point(at = [var 3, var 4])
8853}
8854";
8855
8856        let program = Program::parse(initial_source).unwrap().0.unwrap();
8857        let mut frontend = FrontendState::new();
8858
8859        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8860        let mock_ctx = ExecutorContext::new_mock(None).await;
8861        let version = Version(0);
8862
8863        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8864        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8865        let sketch_id = sketch_object.id;
8866        let sketch = expect_sketch(sketch_object);
8867
8868        let last_point_id = *sketch.segments.last().unwrap();
8869
8870        let (src_delta, _scene_delta) = frontend
8871            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
8872            .await
8873            .unwrap();
8874        // No following item to attach to; the comment is kept inside the sketch
8875        // block as trailing non-code metadata so the user does not lose it.
8876        insta::assert_snapshot!(
8877            "test_delete_last_segment_preserves_pre_comment",
8878            src_delta.text.as_str()
8879        );
8880
8881        ctx.close().await;
8882        mock_ctx.close().await;
8883    }
8884
8885    #[tokio::test(flavor = "multi_thread")]
8886    async fn test_delete_segment_drops_inline_trailing_comment() {
8887        let initial_source = "\
8888sketch(on = XY) {
8889  point(at = [var 1, var 2])
8890  point(at = [var 3, var 4]) // same-line note that gets dropped
8891  point(at = [var 5, var 6])
8892}
8893";
8894
8895        let program = Program::parse(initial_source).unwrap().0.unwrap();
8896        let mut frontend = FrontendState::new();
8897
8898        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8899        let mock_ctx = ExecutorContext::new_mock(None).await;
8900        let version = Version(0);
8901
8902        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8903        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8904        let sketch_id = sketch_object.id;
8905        let sketch = expect_sketch(sketch_object);
8906
8907        let middle_point_id = *sketch.segments.get(1).unwrap();
8908
8909        let (src_delta, _scene_delta) = frontend
8910            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8911            .await
8912            .unwrap();
8913        // The same-line trailing comment is removed along with the deleted code.
8914        assert!(
8915            !src_delta.text.contains("same-line note"),
8916            "inline comment should have been removed: {}",
8917            src_delta.text
8918        );
8919
8920        ctx.close().await;
8921        mock_ctx.close().await;
8922    }
8923
8924    #[tokio::test(flavor = "multi_thread")]
8925    async fn test_delete_segments_preserves_block_comments_across_positions() {
8926        // One test exercising several `delete_body_item_preserving_pre_comments`
8927        // branches at once with `/* ... */` block comments:
8928        //   - first point: leading block comment must migrate to the next item.
8929        //   - first point: same-line trailing block comment must be dropped.
8930        //   - middle point: leading block comment must stay attached after migration.
8931        //   - last point: leading block comment, with no surviving next item,
8932        //     must be converted into a trailing NonCodeNode.
8933        let initial_source = "\
8934sketch(on = XY) {
8935  /* above first - moves to middle */
8936  point(at = [var 1, var 2]) /* same-line on first - dropped */
8937  /* above middle - stays */
8938  point(at = [var 3, var 4])
8939  /* above last - moves to trailing meta */
8940  point(at = [var 5, var 6])
8941}
8942";
8943
8944        let program = Program::parse(initial_source).unwrap().0.unwrap();
8945        let mut frontend = FrontendState::new();
8946
8947        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8948        let mock_ctx = ExecutorContext::new_mock(None).await;
8949        let version = Version(0);
8950
8951        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8952        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8953        let sketch_id = sketch_object.id;
8954        let sketch = expect_sketch(sketch_object);
8955
8956        let first_point_id = *sketch.segments.first().unwrap();
8957        let last_point_id = *sketch.segments.last().unwrap();
8958
8959        let (src_delta, _scene_delta) = frontend
8960            .delete_objects(
8961                &mock_ctx,
8962                version,
8963                sketch_id,
8964                Vec::new(),
8965                vec![first_point_id, last_point_id],
8966            )
8967            .await
8968            .unwrap();
8969        insta::assert_snapshot!(
8970            "test_delete_segments_preserves_block_comments_across_positions",
8971            src_delta.text.as_str()
8972        );
8973
8974        ctx.close().await;
8975        mock_ctx.close().await;
8976    }
8977
8978    #[tokio::test(flavor = "multi_thread")]
8979    async fn test_edit_line_when_editing_its_start_point() {
8980        let initial_source = "\
8981sketch(on = XY) {
8982  line(start = [var 1, var 2], end = [var 3, var 4])
8983}
8984";
8985
8986        let program = Program::parse(initial_source).unwrap().0.unwrap();
8987
8988        let mut frontend = FrontendState::new();
8989
8990        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8991        let mock_ctx = ExecutorContext::new_mock(None).await;
8992        let version = Version(0);
8993
8994        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8995        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8996        let sketch_id = sketch_object.id;
8997        let sketch = expect_sketch(sketch_object);
8998
8999        let point_id = *sketch.segments.first().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_start_point", src_delta.text.as_str());
9022        assert_eq!(scene_delta.new_objects, vec![]);
9023        assert_eq!(scene_delta.new_graph.objects.len(), 5);
9024
9025        ctx.close().await;
9026        mock_ctx.close().await;
9027    }
9028
9029    #[tokio::test(flavor = "multi_thread")]
9030    async fn test_edit_line_when_editing_its_end_point() {
9031        let initial_source = "\
9032sketch(on = XY) {
9033  line(start = [var 1, var 2], end = [var 3, var 4])
9034}
9035";
9036
9037        let program = Program::parse(initial_source).unwrap().0.unwrap();
9038
9039        let mut frontend = FrontendState::new();
9040
9041        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9042        let mock_ctx = ExecutorContext::new_mock(None).await;
9043        let version = Version(0);
9044
9045        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9046        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9047        let sketch_id = sketch_object.id;
9048        let sketch = expect_sketch(sketch_object);
9049        let point_id = *sketch.segments.get(1).unwrap();
9050
9051        let point_ctor = PointCtor {
9052            position: Point2d {
9053                x: Expr::Var(Number {
9054                    value: 5.0,
9055                    units: NumericSuffix::Inch,
9056                }),
9057                y: Expr::Var(Number {
9058                    value: 6.0,
9059                    units: NumericSuffix::Inch,
9060                }),
9061            },
9062        };
9063        let segments = vec![ExistingSegmentCtor {
9064            id: point_id,
9065            ctor: SegmentCtor::Point(point_ctor),
9066        }];
9067        let (src_delta, scene_delta) = frontend
9068            .edit_segments(&mock_ctx, version, sketch_id, segments)
9069            .await
9070            .unwrap();
9071        insta::assert_snapshot!("test_edit_line_when_editing_its_end_point", src_delta.text.as_str());
9072        assert_eq!(scene_delta.new_objects, vec![]);
9073        assert_eq!(
9074            scene_delta.new_graph.objects.len(),
9075            5,
9076            "{:#?}",
9077            scene_delta.new_graph.objects
9078        );
9079
9080        ctx.close().await;
9081        mock_ctx.close().await;
9082    }
9083
9084    #[tokio::test(flavor = "multi_thread")]
9085    async fn test_edit_line_with_coincident_feedback() {
9086        let initial_source = "\
9087sketch(on = XY) {
9088  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9089  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9090  fixed([line1.start, [0, 0]])
9091  coincident([line1.end, line2.start])
9092  equalLength([line1, line2])
9093}
9094";
9095
9096        let program = Program::parse(initial_source).unwrap().0.unwrap();
9097
9098        let mut frontend = FrontendState::new();
9099
9100        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9101        let mock_ctx = ExecutorContext::new_mock(None).await;
9102        let version = Version(0);
9103
9104        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9105        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9106        let sketch_id = sketch_object.id;
9107        let sketch = expect_sketch(sketch_object);
9108        let line2_end_id = *sketch.segments.get(4).unwrap();
9109
9110        let segments = vec![ExistingSegmentCtor {
9111            id: line2_end_id,
9112            ctor: SegmentCtor::Point(PointCtor {
9113                position: Point2d {
9114                    x: Expr::Var(Number {
9115                        value: 9.0,
9116                        units: NumericSuffix::None,
9117                    }),
9118                    y: Expr::Var(Number {
9119                        value: 10.0,
9120                        units: NumericSuffix::None,
9121                    }),
9122                },
9123            }),
9124        }];
9125        let (src_delta, scene_delta) = frontend
9126            .edit_segments(&mock_ctx, version, sketch_id, segments)
9127            .await
9128            .unwrap();
9129        insta::assert_snapshot!("test_edit_line_with_coincident_feedback", src_delta.text.as_str());
9130        assert_eq!(
9131            scene_delta.new_graph.objects.len(),
9132            11,
9133            "{:#?}",
9134            scene_delta.new_graph.objects
9135        );
9136
9137        ctx.close().await;
9138        mock_ctx.close().await;
9139    }
9140
9141    #[tokio::test(flavor = "multi_thread")]
9142    async fn test_edit_segments_persists_solver_feedback_for_next_mock_execute() {
9143        let initial_source = "\
9144sketch(on = XY) {
9145  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9146  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9147  fixed([line1.start, [0, 0]])
9148  coincident([line1.end, line2.start])
9149  equalLength([line1, line2])
9150}
9151";
9152
9153        let program = Program::parse(initial_source).unwrap().0.unwrap();
9154        let mut frontend = FrontendState::new();
9155        let mock_ctx = ExecutorContext::new_mock(None).await;
9156        let version = Version(0);
9157
9158        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9159        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9160        let sketch_id = sketch_object.id;
9161        let sketch = expect_sketch(sketch_object);
9162        let line2_end_id = *sketch.segments.get(4).unwrap();
9163
9164        let segments = vec![ExistingSegmentCtor {
9165            id: line2_end_id,
9166            ctor: SegmentCtor::Point(PointCtor {
9167                position: Point2d {
9168                    x: Expr::Var(Number {
9169                        value: 9.0,
9170                        units: NumericSuffix::None,
9171                    }),
9172                    y: Expr::Var(Number {
9173                        value: 10.0,
9174                        units: NumericSuffix::None,
9175                    }),
9176                },
9177            }),
9178        }];
9179        let (edited_source, _) = frontend
9180            .edit_segments(&mock_ctx, version, sketch_id, segments)
9181            .await
9182            .unwrap();
9183
9184        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9185        assert_eq!(mock_source.text, edited_source.text);
9186
9187        mock_ctx.close().await;
9188    }
9189
9190    /// Preview segment edits should return solved geometry without persisting
9191    /// solver feedback to KCL.
9192    #[tokio::test(flavor = "multi_thread")]
9193    async fn test_preview_edit_segments_does_not_persist_solver_feedback() {
9194        let initial_source = "\
9195sketch(on = XY) {
9196  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9197  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9198  fixed([line1.start, [0, 0]])
9199  coincident([line1.end, line2.start])
9200  equalLength([line1, line2])
9201}
9202";
9203
9204        let program = Program::parse(initial_source).unwrap().0.unwrap();
9205        let mut frontend = FrontendState::new();
9206        let mock_ctx = ExecutorContext::new_mock(None).await;
9207        let version = Version(0);
9208
9209        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9210        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9211        let sketch_id = sketch_object.id;
9212        let sketch = expect_sketch(sketch_object);
9213        let line2_end_id = *sketch.segments.get(4).unwrap();
9214
9215        let segments = vec![ExistingSegmentCtor {
9216            id: line2_end_id,
9217            ctor: SegmentCtor::Point(PointCtor {
9218                position: Point2d {
9219                    x: Expr::Var(Number {
9220                        value: 9.0,
9221                        units: NumericSuffix::None,
9222                    }),
9223                    y: Expr::Var(Number {
9224                        value: 10.0,
9225                        units: NumericSuffix::None,
9226                    }),
9227                },
9228            }),
9229        }];
9230        let (preview_source, preview_delta) = frontend
9231            .edit_segments_with_options(
9232                &mock_ctx,
9233                version,
9234                sketch_id,
9235                segments,
9236                EditSegmentsOptions {
9237                    anchor_segment_ids: Some(vec![line2_end_id]),
9238                    drag_anchors: Vec::new(),
9239                    constraint_label_edits: Vec::new(),
9240                    commit_solved_initial_guesses: false,
9241                },
9242            )
9243            .await
9244            .unwrap();
9245
9246        assert!(
9247            !preview_delta.exec_outcome.var_solutions.is_empty(),
9248            "preview solve should still solve and return geometry feedback"
9249        );
9250        assert!(
9251            preview_source
9252                .text
9253                .contains("line1 = line(start = [var 1, var 2], end = [var 1, var 2])")
9254        );
9255        assert!(
9256            preview_source
9257                .text
9258                .contains("line2 = line(start = [var 5, var 6], end = [var 9, var 10])")
9259        );
9260
9261        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9262        assert_eq!(mock_source.text, preview_source.text);
9263
9264        mock_ctx.close().await;
9265    }
9266
9267    #[tokio::test(flavor = "multi_thread")]
9268    async fn test_add_constraint_persists_solver_feedback_for_next_mock_execute() {
9269        let initial_source = "\
9270sketch(on = XY) {
9271  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
9272}
9273";
9274
9275        let program = Program::parse(initial_source).unwrap().0.unwrap();
9276        let mut frontend = FrontendState::new();
9277        let mock_ctx = ExecutorContext::new_mock(None).await;
9278        let version = Version(0);
9279
9280        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9281        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9282        let sketch_id = sketch_object.id;
9283        let sketch = expect_sketch(sketch_object);
9284        let line_end_id = *sketch.segments.get(1).unwrap();
9285
9286        let constraint = Constraint::Fixed(Fixed {
9287            points: vec![FixedPoint {
9288                point: line_end_id,
9289                position: Point2d {
9290                    x: Number {
9291                        value: 20.0,
9292                        units: NumericSuffix::Mm,
9293                    },
9294                    y: Number {
9295                        value: 0.0,
9296                        units: NumericSuffix::Mm,
9297                    },
9298                },
9299            }],
9300        });
9301        let (constraint_source, _) = frontend
9302            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9303            .await
9304            .unwrap();
9305
9306        assert!(
9307            constraint_source
9308                .text
9309                .contains("line1 = line(start = [var 0, var 0], end = [var 20, var 0])"),
9310            "{}",
9311            constraint_source.text
9312        );
9313        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9314        assert_eq!(mock_source.text, constraint_source.text);
9315
9316        mock_ctx.close().await;
9317    }
9318
9319    #[test]
9320    fn test_no_solver_feedback_preserves_original_source() {
9321        let initial_source = "\
9322@settings(defaultLengthUnit = in, kclVersion = 2.0)
9323cylinder = startSketchOn(XY)
9324    |> circle(center= [0, 0], radius= 22)
9325    |> extrude(length = 14)
9326";
9327        let mut frontend = FrontendState::new();
9328        frontend.program = Program::parse(initial_source).unwrap().0.unwrap();
9329        let outcome = ExecOutcome {
9330            variables: Default::default(),
9331            operations: Default::default(),
9332            artifact_graph: Default::default(),
9333            scene_objects: Default::default(),
9334            source_range_to_object: Default::default(),
9335            var_solutions: Default::default(),
9336            refactor_metadata: Default::default(),
9337            issues: Default::default(),
9338            filenames: Default::default(),
9339            source_files: Default::default(),
9340            default_planes: Default::default(),
9341        };
9342
9343        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9344
9345        assert_eq!(source_delta.text, initial_source);
9346    }
9347
9348    /// Explicit drag anchors should limit which edited points become temporary
9349    /// fixed constraints.
9350    #[tokio::test(flavor = "multi_thread")]
9351    async fn test_edit_segments_with_anchor_ids_limits_drag_fixed_constraints() {
9352        let initial_source = "\
9353sketch(on = XY) {
9354  point1 = point(at = [var 0mm, var 0mm])
9355  point2 = point(at = [var 0mm, var 0mm])
9356  coincident([point1, point2])
9357}
9358";
9359
9360        let program = Program::parse(initial_source).unwrap().0.unwrap();
9361        let mut frontend = FrontendState::new();
9362        let mock_ctx = ExecutorContext::new_mock(None).await;
9363        let version = Version(0);
9364
9365        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9366        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9367        let sketch_id = sketch_object.id;
9368        let sketch = expect_sketch(sketch_object);
9369        let point1_id = sketch.segments[0];
9370        let point2_id = sketch.segments[1];
9371
9372        let segments = vec![
9373            ExistingSegmentCtor {
9374                id: point1_id,
9375                ctor: SegmentCtor::Point(PointCtor {
9376                    position: point_expr_mm(10.0, 0.0),
9377                }),
9378            },
9379            ExistingSegmentCtor {
9380                id: point2_id,
9381                ctor: SegmentCtor::Point(PointCtor {
9382                    position: point_expr_mm(100.0, 0.0),
9383                }),
9384            },
9385        ];
9386        let (_, scene_delta) = frontend
9387            .edit_segments_with_options(
9388                &mock_ctx,
9389                version,
9390                sketch_id,
9391                segments,
9392                EditSegmentsOptions {
9393                    anchor_segment_ids: Some(vec![point1_id]),
9394                    drag_anchors: Vec::new(),
9395                    constraint_label_edits: Vec::new(),
9396                    commit_solved_initial_guesses: true,
9397                },
9398            )
9399            .await
9400            .unwrap();
9401
9402        assert_point_position_close(
9403            point_position(&scene_delta.new_graph, point1_id),
9404            point_number_mm(10.0, 0.0),
9405        );
9406        assert_point_position_close(
9407            point_position(&scene_delta.new_graph, point2_id),
9408            point_number_mm(10.0, 0.0),
9409        );
9410
9411        mock_ctx.close().await;
9412    }
9413
9414    /// Walks a program collecting `(literal_source_range, sketch_var_node_path)`
9415    /// for every SketchVar whose initial NumericLiteral has the given value.
9416    fn collect_sketch_var_literals_with_value(program: &Program, value: f64) -> Vec<(SourceRange, ast::NodePath)> {
9417        use std::cell::RefCell;
9418        struct Collector {
9419            target: f64,
9420            out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9421        }
9422        impl<'a> crate::walk::Visitor<'a> for &Collector {
9423            type Error = crate::front::Error;
9424            fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9425                if let crate::walk::Node::SketchVar(sketch_var) = node
9426                    && let (Some(initial), Some(node_path)) = (&sketch_var.initial, &sketch_var.node_path)
9427                    && (initial.value - self.target).abs() < 1e-9
9428                {
9429                    self.out
9430                        .borrow_mut()
9431                        .push((SourceRange::from(initial.as_ref()), node_path.clone()));
9432                }
9433                for child in node.children().iter() {
9434                    if !child.visit(*self)? {
9435                        return Ok(false);
9436                    }
9437                }
9438                Ok(true)
9439            }
9440        }
9441        let collector = Collector {
9442            target: value,
9443            out: Default::default(),
9444        };
9445        let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9446        collector.out.into_inner()
9447    }
9448
9449    /// Walk a program collecting `(sketch_var_source_range, sketch_var_node_path)`
9450    /// for every SketchVar (including bare `var`).
9451    fn collect_all_sketch_vars(program: &Program) -> Vec<(SourceRange, ast::NodePath)> {
9452        use std::cell::RefCell;
9453        struct Collector {
9454            out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9455        }
9456        impl<'a> crate::walk::Visitor<'a> for &Collector {
9457            type Error = crate::front::Error;
9458            fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9459                if let crate::walk::Node::SketchVar(sketch_var) = node
9460                    && let Some(node_path) = &sketch_var.node_path
9461                {
9462                    self.out
9463                        .borrow_mut()
9464                        .push((SourceRange::from(sketch_var), node_path.clone()));
9465                }
9466                for child in node.children().iter() {
9467                    if !child.visit(*self)? {
9468                        return Ok(false);
9469                    }
9470                }
9471                Ok(true)
9472            }
9473        }
9474        let collector = Collector {
9475            out: Default::default(),
9476        };
9477        let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9478        collector.out.into_inner()
9479    }
9480
9481    fn empty_exec_outcome_with_var_solutions(
9482        var_solutions: Vec<(SourceRange, Option<ast::NodePath>, Number)>,
9483    ) -> ExecOutcome {
9484        ExecOutcome {
9485            variables: Default::default(),
9486            operations: Default::default(),
9487            artifact_graph: Default::default(),
9488            scene_objects: Default::default(),
9489            source_range_to_object: Default::default(),
9490            var_solutions,
9491            refactor_metadata: Default::default(),
9492            issues: Default::default(),
9493            filenames: Default::default(),
9494            source_files: Default::default(),
9495            default_planes: Default::default(),
9496        }
9497    }
9498
9499    /// Happy path: commit a var solution to a `var N` inside a sketch block
9500    /// using a correct NodePath. Confirms the node-path code path produces the
9501    /// expected source mutation.
9502    #[test]
9503    fn test_commit_var_solution_by_node_path_updates_sketch_var() {
9504        let initial_source = "\
9505sketch(on = XY) {
9506  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9507}
9508";
9509        let program = Program::parse(initial_source).unwrap().0.unwrap();
9510        let matches = collect_sketch_var_literals_with_value(&program, 10.0);
9511        assert_eq!(matches.len(), 1, "expected exactly one `var 10mm`");
9512        let (literal_range, node_path) = matches.into_iter().next().unwrap();
9513
9514        let mut frontend = FrontendState::new();
9515        frontend.program = program;
9516
9517        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9518            literal_range,
9519            Some(node_path),
9520            Number {
9521                value: 25.0,
9522                units: NumericSuffix::Mm,
9523            },
9524        )]);
9525
9526        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9527
9528        insta::assert_snapshot!(
9529            "test_commit_var_solution_by_node_path_updates_sketch_var",
9530            source_delta.text
9531        );
9532    }
9533
9534    /// Whitespace inserted earlier in the source shifts the original SketchVar
9535    /// SourceRange. With NodePath propagation the commit should still target
9536    /// the right `var`. We simulate this by collecting node_paths against a
9537    /// "compact" source, then loading the frontend with a "padded" source
9538    /// (whose byte offsets differ), and feeding the original (now stale)
9539    /// source range plus the correct node_path back into the commit.
9540    #[test]
9541    fn test_commit_var_solution_survives_whitespace_shift_earlier_in_file() {
9542        let compact_source = "\
9543sketch(on = XY) {
9544  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9545}
9546";
9547        let padded_source = "\
9548// added comment\n// added comment\n\nsketch(on = XY) {
9549  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9550}
9551";
9552        let compact_program = Program::parse(compact_source).unwrap().0.unwrap();
9553        let padded_program = Program::parse(padded_source).unwrap().0.unwrap();
9554
9555        let compact_match = collect_sketch_var_literals_with_value(&compact_program, 10.0)
9556            .into_iter()
9557            .next()
9558            .expect("expected `var 10mm` in compact source");
9559        let padded_match = collect_sketch_var_literals_with_value(&padded_program, 10.0)
9560            .into_iter()
9561            .next()
9562            .expect("expected `var 10mm` in padded source");
9563
9564        assert_ne!(
9565            compact_match.0, padded_match.0,
9566            "byte offsets must differ for this test to be meaningful"
9567        );
9568        assert_eq!(
9569            compact_match.1, padded_match.1,
9570            "node paths must agree across whitespace; that's the whole point of NodePath",
9571        );
9572
9573        let mut frontend = FrontendState::new();
9574        frontend.program = padded_program;
9575
9576        // Stale source range from the compact source + correct node_path.
9577        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9578            compact_match.0,
9579            Some(compact_match.1),
9580            Number {
9581                value: 30.0,
9582                units: NumericSuffix::Mm,
9583            },
9584        )]);
9585
9586        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9587
9588        insta::assert_snapshot!(
9589            "test_commit_var_solution_survives_whitespace_shift_earlier_in_file",
9590            source_delta.text
9591        );
9592    }
9593
9594    /// When multiple `var` declarations exist and the stale source range
9595    /// happens to land on a *different* var, the node_path must take
9596    /// precedence and the right var gets updated.
9597    #[test]
9598    fn test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var() {
9599        let initial_source = "\
9600sketch(on = XY) {
9601  line1 = line(start = [var 10mm, var 0mm], end = [var 20mm, var 0mm])
9602}
9603";
9604        let program = Program::parse(initial_source).unwrap().0.unwrap();
9605
9606        let var_10 = collect_sketch_var_literals_with_value(&program, 10.0)
9607            .into_iter()
9608            .next()
9609            .expect("expected `var 10mm`");
9610        let var_20 = collect_sketch_var_literals_with_value(&program, 20.0)
9611            .into_iter()
9612            .next()
9613            .expect("expected `var 20mm`");
9614
9615        let mut frontend = FrontendState::new();
9616        frontend.program = program;
9617
9618        // Use var 20mm's source range, but var 10mm's node_path. node_path wins.
9619        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9620            var_20.0,
9621            Some(var_10.1),
9622            Number {
9623                value: 33.0,
9624                units: NumericSuffix::Mm,
9625            },
9626        )]);
9627
9628        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9629
9630        insta::assert_snapshot!(
9631            "test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var",
9632            source_delta.text
9633        );
9634    }
9635
9636    /// Bare `var` (no initial literal) is only locatable via node_path. With
9637    /// the EditVarInitialValue handler now operating on the SketchVar node, a
9638    /// solver solution should fill the initial value in. The
9639    /// `@settings(experimentalFeatures = allow)` is required because bare `var`
9640    /// is gated as an experimental feature; without it the re-parse of the
9641    /// recast source rejects bare `var` declarations.
9642    #[test]
9643    fn test_commit_var_solution_writes_back_into_bare_var() {
9644        let initial_source = "\
9645@settings(experimentalFeatures = allow, kclVersion = 2.0)
9646sketch(on = XY) {
9647  line1 = line(start = [var, var 0mm], end = [var 10mm, var 0])
9648}
9649";
9650        let program = Program::parse(initial_source).unwrap().0.unwrap();
9651
9652        // Pick the first bare `var`; collect_all_sketch_vars returns every
9653        // SketchVar, including bare ones.
9654        let bare = collect_all_sketch_vars(&program)
9655            .into_iter()
9656            .find(|(range, _)| {
9657                // The bare `var` is exactly the 3 characters "var".
9658                range.end() - range.start() == 3
9659            })
9660            .expect("expected at least one bare `var`");
9661
9662        let mut frontend = FrontendState::new();
9663        frontend.program = program;
9664
9665        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9666            bare.0,
9667            Some(bare.1),
9668            Number {
9669                value: 7.0,
9670                units: NumericSuffix::Mm,
9671            },
9672        )]);
9673
9674        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9675
9676        // Default length unit (mm; no `@settings(defaultLengthUnit = …)`) is
9677        // written as an explicit suffix so the bare var commits with units.
9678        // The recast adds a blank line after the `@settings` annotation.
9679        insta::assert_snapshot!("test_commit_var_solution_writes_back_into_bare_var", source_delta.text);
9680    }
9681
9682    #[tokio::test(flavor = "multi_thread")]
9683    async fn test_delete_point_without_var() {
9684        let initial_source = "\
9685sketch(on = XY) {
9686  point(at = [var 1, var 2])
9687  point(at = [var 3, var 4])
9688  point(at = [var 5, var 6])
9689}
9690";
9691
9692        let program = Program::parse(initial_source).unwrap().0.unwrap();
9693
9694        let mut frontend = FrontendState::new();
9695
9696        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9697        let mock_ctx = ExecutorContext::new_mock(None).await;
9698        let version = Version(0);
9699
9700        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9701        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9702        let sketch_id = sketch_object.id;
9703        let sketch = expect_sketch(sketch_object);
9704
9705        let point_id = *sketch.segments.get(1).unwrap();
9706
9707        let (src_delta, scene_delta) = frontend
9708            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9709            .await
9710            .unwrap();
9711        insta::assert_snapshot!("test_delete_point_without_var", src_delta.text.as_str());
9712        assert_eq!(scene_delta.new_objects, vec![]);
9713        assert_eq!(scene_delta.new_graph.objects.len(), 4);
9714
9715        ctx.close().await;
9716        mock_ctx.close().await;
9717    }
9718
9719    #[tokio::test(flavor = "multi_thread")]
9720    async fn test_delete_point_with_var() {
9721        let initial_source = "\
9722sketch(on = XY) {
9723  point(at = [var 1, var 2])
9724  point1 = point(at = [var 3, var 4])
9725  point(at = [var 5, var 6])
9726}
9727";
9728
9729        let program = Program::parse(initial_source).unwrap().0.unwrap();
9730
9731        let mut frontend = FrontendState::new();
9732
9733        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9734        let mock_ctx = ExecutorContext::new_mock(None).await;
9735        let version = Version(0);
9736
9737        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9738        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9739        let sketch_id = sketch_object.id;
9740        let sketch = expect_sketch(sketch_object);
9741
9742        let point_id = *sketch.segments.get(1).unwrap();
9743
9744        let (src_delta, scene_delta) = frontend
9745            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9746            .await
9747            .unwrap();
9748        insta::assert_snapshot!("test_delete_point_with_var", src_delta.text.as_str());
9749        assert_eq!(scene_delta.new_objects, vec![]);
9750        assert_eq!(scene_delta.new_graph.objects.len(), 4);
9751
9752        ctx.close().await;
9753        mock_ctx.close().await;
9754    }
9755
9756    #[tokio::test(flavor = "multi_thread")]
9757    async fn test_delete_multiple_points() {
9758        let initial_source = "\
9759sketch(on = XY) {
9760  point(at = [var 1, var 2])
9761  point1 = point(at = [var 3, var 4])
9762  point(at = [var 5, var 6])
9763}
9764";
9765
9766        let program = Program::parse(initial_source).unwrap().0.unwrap();
9767
9768        let mut frontend = FrontendState::new();
9769
9770        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9771        let mock_ctx = ExecutorContext::new_mock(None).await;
9772        let version = Version(0);
9773
9774        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9775        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9776        let sketch_id = sketch_object.id;
9777
9778        let sketch = expect_sketch(sketch_object);
9779
9780        let point1_id = *sketch.segments.first().unwrap();
9781        let point2_id = *sketch.segments.get(1).unwrap();
9782
9783        let (src_delta, scene_delta) = frontend
9784            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
9785            .await
9786            .unwrap();
9787        insta::assert_snapshot!("test_delete_multiple_points", src_delta.text.as_str());
9788        assert_eq!(scene_delta.new_objects, vec![]);
9789        assert_eq!(scene_delta.new_graph.objects.len(), 3);
9790
9791        ctx.close().await;
9792        mock_ctx.close().await;
9793    }
9794
9795    #[tokio::test(flavor = "multi_thread")]
9796    async fn test_delete_coincident_constraint() {
9797        let initial_source = "\
9798sketch(on = XY) {
9799  point1 = point(at = [var 1, var 2])
9800  point2 = point(at = [var 3, var 4])
9801  coincident([point1, point2])
9802  point(at = [var 5, var 6])
9803}
9804";
9805
9806        let program = Program::parse(initial_source).unwrap().0.unwrap();
9807
9808        let mut frontend = FrontendState::new();
9809
9810        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9811        let mock_ctx = ExecutorContext::new_mock(None).await;
9812        let version = Version(0);
9813
9814        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9815        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9816        let sketch_id = sketch_object.id;
9817        let sketch = expect_sketch(sketch_object);
9818
9819        let coincident_id = *sketch.constraints.first().unwrap();
9820
9821        let (src_delta, scene_delta) = frontend
9822            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
9823            .await
9824            .unwrap();
9825        insta::assert_snapshot!("test_delete_coincident_constraint", src_delta.text.as_str());
9826        assert_eq!(scene_delta.new_objects, vec![]);
9827        assert_eq!(scene_delta.new_graph.objects.len(), 5);
9828
9829        ctx.close().await;
9830        mock_ctx.close().await;
9831    }
9832
9833    #[tokio::test(flavor = "multi_thread")]
9834    async fn test_delete_line_cascades_to_coincident_constraint() {
9835        let initial_source = "\
9836sketch(on = XY) {
9837  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9838  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9839  coincident([line1.end, line2.start])
9840}
9841";
9842
9843        let program = Program::parse(initial_source).unwrap().0.unwrap();
9844
9845        let mut frontend = FrontendState::new();
9846
9847        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9848        let mock_ctx = ExecutorContext::new_mock(None).await;
9849        let version = Version(0);
9850
9851        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9852        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9853        let sketch_id = sketch_object.id;
9854        let sketch = expect_sketch(sketch_object);
9855        let line_id = *sketch.segments.get(5).unwrap();
9856
9857        let (src_delta, scene_delta) = frontend
9858            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9859            .await
9860            .unwrap();
9861        insta::assert_snapshot!(
9862            "test_delete_line_cascades_to_coincident_constraint",
9863            src_delta.text.as_str()
9864        );
9865        assert_eq!(
9866            scene_delta.new_graph.objects.len(),
9867            5,
9868            "{:#?}",
9869            scene_delta.new_graph.objects
9870        );
9871
9872        ctx.close().await;
9873        mock_ctx.close().await;
9874    }
9875
9876    #[tokio::test(flavor = "multi_thread")]
9877    async fn test_delete_line_cascades_to_distance_constraint() {
9878        let initial_source = "\
9879sketch(on = XY) {
9880  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9881  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9882  distance([line1.end, line2.start]) == 10mm
9883}
9884";
9885
9886        let program = Program::parse(initial_source).unwrap().0.unwrap();
9887
9888        let mut frontend = FrontendState::new();
9889
9890        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9891        let mock_ctx = ExecutorContext::new_mock(None).await;
9892        let version = Version(0);
9893
9894        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9895        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9896        let sketch_id = sketch_object.id;
9897        let sketch = expect_sketch(sketch_object);
9898        let line_id = *sketch.segments.get(5).unwrap();
9899
9900        let (src_delta, scene_delta) = frontend
9901            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9902            .await
9903            .unwrap();
9904        insta::assert_snapshot!(
9905            "test_delete_line_cascades_to_distance_constraint",
9906            src_delta.text.as_str()
9907        );
9908        assert_eq!(
9909            scene_delta.new_graph.objects.len(),
9910            5,
9911            "{:#?}",
9912            scene_delta.new_graph.objects
9913        );
9914
9915        ctx.close().await;
9916        mock_ctx.close().await;
9917    }
9918
9919    #[tokio::test(flavor = "multi_thread")]
9920    async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
9921        let initial_source = "\
9922sketch(on = XY) {
9923  point1 = point(at = [var 1, var 2])
9924  point2 = point(at = [var 3, var 4])
9925  horizontalDistance([point1, point2]) == 10mm
9926}
9927";
9928
9929        let program = Program::parse(initial_source).unwrap().0.unwrap();
9930
9931        let mut frontend = FrontendState::new();
9932
9933        let mock_ctx = ExecutorContext::new_mock(None).await;
9934        let version = Version(0);
9935
9936        frontend.program = program.clone();
9937        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9938        frontend.update_state_after_exec(outcome, true);
9939        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9940        let sketch_id = sketch_object.id;
9941        let sketch = expect_sketch(sketch_object);
9942        let point2_id = *sketch.segments.get(1).unwrap();
9943
9944        let (src_delta, scene_delta) = frontend
9945            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
9946            .await
9947            .unwrap();
9948        insta::assert_snapshot!(
9949            "test_delete_point_cascades_to_horizontal_distance_constraint",
9950            src_delta.text.as_str()
9951        );
9952        assert_eq!(
9953            scene_delta.new_graph.objects.len(),
9954            3,
9955            "{:#?}",
9956            scene_delta.new_graph.objects
9957        );
9958
9959        mock_ctx.close().await;
9960    }
9961
9962    #[tokio::test(flavor = "multi_thread")]
9963    async fn test_delete_line_cascades_to_fixed_constraint() {
9964        let initial_source = "\
9965sketch(on = XY) {
9966  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9967  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9968  fixed([line1.start, [0, 0]])
9969}
9970";
9971
9972        let program = Program::parse(initial_source).unwrap().0.unwrap();
9973
9974        let mut frontend = FrontendState::new();
9975
9976        let mock_ctx = ExecutorContext::new_mock(None).await;
9977        let version = Version(0);
9978
9979        frontend.program = program.clone();
9980        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9981        frontend.update_state_after_exec(outcome, true);
9982        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9983        let sketch_id = sketch_object.id;
9984        let sketch = expect_sketch(sketch_object);
9985        let line1_id = *sketch.segments.get(2).unwrap();
9986
9987        let (src_delta, scene_delta) = frontend
9988            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9989            .await
9990            .unwrap();
9991        insta::assert_snapshot!("test_delete_line_cascades_to_fixed_constraint", src_delta.text.as_str());
9992        assert_eq!(
9993            scene_delta.new_graph.objects.len(),
9994            5,
9995            "{:#?}",
9996            scene_delta.new_graph.objects
9997        );
9998
9999        mock_ctx.close().await;
10000    }
10001
10002    #[tokio::test(flavor = "multi_thread")]
10003    async fn test_delete_line_cascades_to_midpoint_constraint() {
10004        let initial_source = "\
10005sketch(on = XY) {
10006  point1 = point(at = [var 1, var 2])
10007  line1 = line(start = [var 0, var 0], end = [var 6, var 4])
10008  midpoint(line1, point = point1)
10009}
10010";
10011
10012        let program = Program::parse(initial_source).unwrap().0.unwrap();
10013
10014        let mut frontend = FrontendState::new();
10015
10016        let mock_ctx = ExecutorContext::new_mock(None).await;
10017        let version = Version(0);
10018
10019        frontend.program = program.clone();
10020        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10021        frontend.update_state_after_exec(outcome, true);
10022        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10023        let sketch_id = sketch_object.id;
10024        let sketch = expect_sketch(sketch_object);
10025        let line1_id = *sketch.segments.get(3).unwrap();
10026
10027        let (src_delta, scene_delta) = frontend
10028            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10029            .await
10030            .unwrap();
10031        insta::assert_snapshot!(
10032            "test_delete_line_cascades_to_midpoint_constraint",
10033            src_delta.text.as_str()
10034        );
10035        assert_eq!(
10036            scene_delta.new_graph.objects.len(),
10037            3,
10038            "{:#?}",
10039            scene_delta.new_graph.objects
10040        );
10041
10042        mock_ctx.close().await;
10043    }
10044
10045    #[tokio::test(flavor = "multi_thread")]
10046    async fn test_delete_point_preserves_multiline_coincident_constraint() {
10047        let initial_source = "\
10048sketch(on = XY) {
10049  point1 = point(at = [var 1, var 2])
10050  point2 = point(at = [var 3, var 4])
10051  point3 = point(at = [var 5, var 6])
10052  coincident([point1, point2, point3])
10053}
10054";
10055
10056        let program = Program::parse(initial_source).unwrap().0.unwrap();
10057
10058        let mut frontend = FrontendState::new();
10059
10060        let mock_ctx = ExecutorContext::new_mock(None).await;
10061        let version = Version(0);
10062
10063        frontend.program = program.clone();
10064        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10065        frontend.update_state_after_exec(outcome, true);
10066        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10067        let sketch_id = sketch_object.id;
10068        let sketch = expect_sketch(sketch_object);
10069        let point3_id = *sketch.segments.get(2).unwrap();
10070
10071        let (src_delta, scene_delta) = frontend
10072            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
10073            .await
10074            .unwrap();
10075        assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
10076        assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
10077        assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
10078        assert!(
10079            src_delta.text.contains("coincident([point1, point2])"),
10080            "{}",
10081            src_delta.text
10082        );
10083
10084        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10085        let sketch = expect_sketch(sketch_object);
10086        assert_eq!(sketch.segments.len(), 2);
10087        assert_eq!(sketch.constraints.len(), 1);
10088
10089        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10090        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10091            panic!("Expected constraint object");
10092        };
10093        let Constraint::Coincident(coincident) = constraint else {
10094            panic!("Expected coincident constraint");
10095        };
10096        assert_eq!(
10097            coincident.segments,
10098            sketch
10099                .segments
10100                .iter()
10101                .copied()
10102                .map(Into::into)
10103                .collect::<Vec<ConstraintSegment>>()
10104        );
10105
10106        mock_ctx.close().await;
10107    }
10108
10109    #[tokio::test(flavor = "multi_thread")]
10110    async fn test_delete_line_preserves_multiline_equal_length_constraint() {
10111        let initial_source = "\
10112sketch(on = XY) {
10113  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10114  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10115  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10116  equalLength([line1, line2, line3])
10117}
10118";
10119
10120        let program = Program::parse(initial_source).unwrap().0.unwrap();
10121
10122        let mut frontend = FrontendState::new();
10123
10124        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10125        let mock_ctx = ExecutorContext::new_mock(None).await;
10126        let version = Version(0);
10127
10128        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10129        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10130        let sketch_id = sketch_object.id;
10131        let sketch = expect_sketch(sketch_object);
10132        let line3_id = *sketch.segments.get(8).unwrap();
10133
10134        let (src_delta, scene_delta) = frontend
10135            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10136            .await
10137            .unwrap();
10138        insta::assert_snapshot!(
10139            "test_delete_line_preserves_multiline_equal_length_constraint",
10140            src_delta.text.as_str()
10141        );
10142
10143        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10144        let sketch = expect_sketch(sketch_object);
10145        assert_eq!(sketch.constraints.len(), 1);
10146
10147        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10148        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10149            panic!("Expected constraint object");
10150        };
10151        let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
10152            panic!("Expected lines equal length constraint");
10153        };
10154        assert_eq!(lines_equal_length.lines.len(), 2);
10155
10156        ctx.close().await;
10157        mock_ctx.close().await;
10158    }
10159
10160    #[tokio::test(flavor = "multi_thread")]
10161    async fn test_delete_line_preserves_multiline_horizontal_constraint() {
10162        let initial_source = "\
10163sketch(on = XY) {
10164  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10165  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10166  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10167  horizontal([line1.end, line2.start, line3.start])
10168}
10169";
10170
10171        let program = Program::parse(initial_source).unwrap().0.unwrap();
10172
10173        let mut frontend = FrontendState::new();
10174
10175        let mock_ctx = ExecutorContext::new_mock(None).await;
10176        let version = Version(0);
10177
10178        frontend.program = program.clone();
10179        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10180        frontend.update_state_after_exec(outcome, true);
10181        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10182        let sketch_id = sketch_object.id;
10183        let sketch = expect_sketch(sketch_object);
10184        let line1_id = *sketch.segments.get(2).unwrap();
10185
10186        let (src_delta, scene_delta) = frontend
10187            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10188            .await
10189            .unwrap();
10190        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10191        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10192        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10193        assert!(
10194            src_delta.text.contains("horizontal([line2.start, line3.start])"),
10195            "{}",
10196            src_delta.text
10197        );
10198
10199        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10200        let sketch = expect_sketch(sketch_object);
10201        assert_eq!(sketch.constraints.len(), 1);
10202
10203        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10204        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10205            panic!("Expected constraint object");
10206        };
10207        let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
10208            panic!("Expected horizontal points constraint");
10209        };
10210        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10211        assert_eq!(*points, remaining_points);
10212
10213        mock_ctx.close().await;
10214    }
10215
10216    #[tokio::test(flavor = "multi_thread")]
10217    async fn test_delete_line_preserves_multiline_vertical_constraint() {
10218        let initial_source = "\
10219sketch(on = XY) {
10220  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10221  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10222  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10223  vertical([line1.end, line2.start, line3.start])
10224}
10225";
10226
10227        let program = Program::parse(initial_source).unwrap().0.unwrap();
10228
10229        let mut frontend = FrontendState::new();
10230
10231        let mock_ctx = ExecutorContext::new_mock(None).await;
10232        let version = Version(0);
10233
10234        frontend.program = program.clone();
10235        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10236        frontend.update_state_after_exec(outcome, true);
10237        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10238        let sketch_id = sketch_object.id;
10239        let sketch = expect_sketch(sketch_object);
10240        let line1_id = *sketch.segments.get(2).unwrap();
10241
10242        let (src_delta, scene_delta) = frontend
10243            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10244            .await
10245            .unwrap();
10246        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10247        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10248        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10249        assert!(
10250            src_delta.text.contains("vertical([line2.start, line3.start])"),
10251            "{}",
10252            src_delta.text
10253        );
10254
10255        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10256        let sketch = expect_sketch(sketch_object);
10257        assert_eq!(sketch.constraints.len(), 1);
10258
10259        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10260        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10261            panic!("Expected constraint object");
10262        };
10263        let Constraint::Vertical(Vertical::Points { points }) = constraint else {
10264            panic!("Expected vertical points constraint");
10265        };
10266        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10267        assert_eq!(*points, remaining_points);
10268
10269        mock_ctx.close().await;
10270    }
10271
10272    #[tokio::test(flavor = "multi_thread")]
10273    async fn test_delete_line_preserves_multiline_coincident_constraint() {
10274        let initial_source = "\
10275sketch(on = XY) {
10276  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10277  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10278  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10279  coincident([line1.end, line2.start, line3.start])
10280}
10281";
10282
10283        let program = Program::parse(initial_source).unwrap().0.unwrap();
10284
10285        let mut frontend = FrontendState::new();
10286
10287        let mock_ctx = ExecutorContext::new_mock(None).await;
10288        let version = Version(0);
10289
10290        frontend.program = program.clone();
10291        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10292        frontend.update_state_after_exec(outcome, true);
10293        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10294        let sketch_id = sketch_object.id;
10295        let sketch = expect_sketch(sketch_object);
10296        let line1_id = *sketch.segments.get(2).unwrap();
10297
10298        let (src_delta, scene_delta) = frontend
10299            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10300            .await
10301            .unwrap();
10302        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10303        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10304        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10305        assert!(
10306            src_delta.text.contains("coincident([line2.start, line3.start])"),
10307            "{}",
10308            src_delta.text
10309        );
10310
10311        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10312        let sketch = expect_sketch(sketch_object);
10313        assert_eq!(sketch.constraints.len(), 1);
10314
10315        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10316        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10317            panic!("Expected constraint object");
10318        };
10319        let Constraint::Coincident(coincident) = constraint else {
10320            panic!("Expected coincident constraint");
10321        };
10322        let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10323        assert_eq!(coincident.segments, remaining_segments);
10324
10325        mock_ctx.close().await;
10326    }
10327
10328    #[tokio::test(flavor = "multi_thread")]
10329    async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
10330        let initial_source = "\
10331sketch(on = XY) {
10332  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10333  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10334  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10335  equalLength([line1, line2, line3])
10336}
10337";
10338
10339        let program = Program::parse(initial_source).unwrap().0.unwrap();
10340
10341        let mut frontend = FrontendState::new();
10342
10343        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10344        let mock_ctx = ExecutorContext::new_mock(None).await;
10345        let version = Version(0);
10346
10347        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10348        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10349        let sketch_id = sketch_object.id;
10350        let sketch = expect_sketch(sketch_object);
10351        let line2_id = *sketch.segments.get(5).unwrap();
10352        let line3_id = *sketch.segments.get(8).unwrap();
10353
10354        let (src_delta, scene_delta) = frontend
10355            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10356            .await
10357            .unwrap();
10358        insta::assert_snapshot!(
10359            "test_delete_lines_removes_multiline_equal_length_constraint_below_minimum",
10360            src_delta.text.as_str()
10361        );
10362
10363        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10364        let sketch = expect_sketch(sketch_object);
10365        assert!(sketch.constraints.is_empty());
10366
10367        ctx.close().await;
10368        mock_ctx.close().await;
10369    }
10370
10371    #[tokio::test(flavor = "multi_thread")]
10372    async fn test_delete_line_preserves_multiline_parallel_constraint() {
10373        let initial_source = "\
10374sketch(on = XY) {
10375  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10376  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10377  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10378  parallel([line1, line2, line3])
10379}
10380";
10381
10382        let program = Program::parse(initial_source).unwrap().0.unwrap();
10383
10384        let mut frontend = FrontendState::new();
10385
10386        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10387        let mock_ctx = ExecutorContext::new_mock(None).await;
10388        let version = Version(0);
10389
10390        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10391        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10392        let sketch_id = sketch_object.id;
10393        let sketch = expect_sketch(sketch_object);
10394        let line3_id = *sketch.segments.get(8).unwrap();
10395
10396        let (src_delta, scene_delta) = frontend
10397            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10398            .await
10399            .unwrap();
10400        insta::assert_snapshot!(
10401            "test_delete_line_preserves_multiline_parallel_constraint",
10402            src_delta.text.as_str()
10403        );
10404
10405        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10406        let sketch = expect_sketch(sketch_object);
10407        assert_eq!(sketch.constraints.len(), 1);
10408
10409        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10410        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10411            panic!("Expected constraint object");
10412        };
10413        let Constraint::Parallel(parallel) = constraint else {
10414            panic!("Expected parallel constraint");
10415        };
10416        assert_eq!(parallel.lines.len(), 2);
10417
10418        ctx.close().await;
10419        mock_ctx.close().await;
10420    }
10421
10422    #[tokio::test(flavor = "multi_thread")]
10423    async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
10424        let initial_source = "\
10425sketch(on = XY) {
10426  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10427  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10428  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10429  parallel([line1, line2, line3])
10430}
10431";
10432
10433        let program = Program::parse(initial_source).unwrap().0.unwrap();
10434
10435        let mut frontend = FrontendState::new();
10436
10437        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10438        let mock_ctx = ExecutorContext::new_mock(None).await;
10439        let version = Version(0);
10440
10441        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10442        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10443        let sketch_id = sketch_object.id;
10444        let sketch = expect_sketch(sketch_object);
10445        let line2_id = *sketch.segments.get(5).unwrap();
10446        let line3_id = *sketch.segments.get(8).unwrap();
10447
10448        let (src_delta, scene_delta) = frontend
10449            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10450            .await
10451            .unwrap();
10452        insta::assert_snapshot!(
10453            "test_delete_lines_removes_multiline_parallel_constraint_below_minimum",
10454            src_delta.text.as_str()
10455        );
10456
10457        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10458        let sketch = expect_sketch(sketch_object);
10459        assert!(sketch.constraints.is_empty());
10460
10461        ctx.close().await;
10462        mock_ctx.close().await;
10463    }
10464
10465    #[tokio::test(flavor = "multi_thread")]
10466    async fn test_delete_line_line_coincident_constraint() {
10467        let initial_source = "\
10468sketch(on = XY) {
10469  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10470  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10471  coincident([line1, line2])
10472}
10473";
10474
10475        let program = Program::parse(initial_source).unwrap().0.unwrap();
10476
10477        let mut frontend = FrontendState::new();
10478
10479        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10480        let mock_ctx = ExecutorContext::new_mock(None).await;
10481        let version = Version(0);
10482
10483        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10484        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10485        let sketch_id = sketch_object.id;
10486        let sketch = expect_sketch(sketch_object);
10487
10488        let coincident_id = *sketch.constraints.first().unwrap();
10489
10490        let (src_delta, scene_delta) = frontend
10491            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
10492            .await
10493            .unwrap();
10494        insta::assert_snapshot!("test_delete_line_line_coincident_constraint", src_delta.text.as_str());
10495        assert_eq!(scene_delta.new_objects, vec![]);
10496        assert_eq!(scene_delta.new_graph.objects.len(), 8);
10497
10498        ctx.close().await;
10499        mock_ctx.close().await;
10500    }
10501
10502    #[tokio::test(flavor = "multi_thread")]
10503    async fn test_two_points_coincident() {
10504        let initial_source = "\
10505sketch(on = XY) {
10506  point1 = point(at = [var 1, var 2])
10507  point(at = [3, 4])
10508}
10509";
10510
10511        let program = Program::parse(initial_source).unwrap().0.unwrap();
10512
10513        let mut frontend = FrontendState::new();
10514
10515        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10516        let mock_ctx = ExecutorContext::new_mock(None).await;
10517        let version = Version(0);
10518
10519        frontend.hack_set_program(&ctx, program).await.unwrap();
10520        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10521        let sketch_id = sketch_object.id;
10522        let sketch = expect_sketch(sketch_object);
10523        let point0_id = *sketch.segments.first().unwrap();
10524        let point1_id = *sketch.segments.get(1).unwrap();
10525
10526        let constraint = Constraint::Coincident(Coincident {
10527            segments: vec![point0_id.into(), point1_id.into()],
10528        });
10529        let (src_delta, scene_delta) = frontend
10530            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10531            .await
10532            .unwrap();
10533        insta::assert_snapshot!("test_two_points_coincident", src_delta.text.as_str());
10534        assert_eq!(
10535            scene_delta.new_graph.objects.len(),
10536            5,
10537            "{:#?}",
10538            scene_delta.new_graph.objects
10539        );
10540
10541        ctx.close().await;
10542        mock_ctx.close().await;
10543    }
10544
10545    #[tokio::test(flavor = "multi_thread")]
10546    async fn test_three_points_coincident() {
10547        let initial_source = "\
10548sketch(on = XY) {
10549  point1 = point(at = [var 1, var 2])
10550  point(at = [var 3, var 4])
10551  point(at = [var 5, var 6])
10552}
10553";
10554
10555        let program = Program::parse(initial_source).unwrap().0.unwrap();
10556
10557        let mut frontend = FrontendState::new();
10558
10559        let mock_ctx = ExecutorContext::new_mock(None).await;
10560        let version = Version(0);
10561
10562        frontend.program = program.clone();
10563        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10564        frontend.update_state_after_exec(outcome, true);
10565        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10566        let sketch_id = sketch_object.id;
10567        let sketch = expect_sketch(sketch_object);
10568        let segments = sketch
10569            .segments
10570            .iter()
10571            .take(3)
10572            .copied()
10573            .map(Into::into)
10574            .collect::<Vec<ConstraintSegment>>();
10575
10576        let constraint = Constraint::Coincident(Coincident {
10577            segments: segments.clone(),
10578        });
10579        let (src_delta, scene_delta) = frontend
10580            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10581            .await
10582            .unwrap();
10583        insta::assert_snapshot!("test_three_points_coincident", src_delta.text.as_str());
10584
10585        let constraint_object = scene_delta
10586            .new_graph
10587            .objects
10588            .iter()
10589            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10590            .unwrap();
10591
10592        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10593            panic!("expected a constraint object");
10594        };
10595
10596        assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10597
10598        mock_ctx.close().await;
10599    }
10600
10601    #[tokio::test(flavor = "multi_thread")]
10602    async fn test_source_with_three_point_coincident_tracks_all_segments() {
10603        let initial_source = "\
10604sketch(on = XY) {
10605  point1 = point(at = [var 1, var 2])
10606  point2 = point(at = [var 3, var 4])
10607  point3 = point(at = [var 5, var 6])
10608  coincident([point1, point2, point3])
10609}
10610";
10611
10612        let program = Program::parse(initial_source).unwrap().0.unwrap();
10613
10614        let mut frontend = FrontendState::new();
10615
10616        let ctx = ExecutorContext::new_mock(None).await;
10617        frontend.program = program.clone();
10618        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10619        frontend.update_state_after_exec(outcome, true);
10620
10621        let constraint_object = frontend
10622            .scene_graph
10623            .objects
10624            .iter()
10625            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10626            .unwrap();
10627        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10628            panic!("expected a constraint object");
10629        };
10630
10631        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10632        let sketch = expect_sketch(sketch_object);
10633        let expected_segments = sketch
10634            .segments
10635            .iter()
10636            .take(3)
10637            .copied()
10638            .map(Into::into)
10639            .collect::<Vec<ConstraintSegment>>();
10640
10641        assert_eq!(
10642            constraint,
10643            &Constraint::Coincident(Coincident {
10644                segments: expected_segments,
10645            })
10646        );
10647
10648        ctx.close().await;
10649    }
10650
10651    #[tokio::test(flavor = "multi_thread")]
10652    async fn test_point_origin_coincident_preserves_order() {
10653        let initial_source = "\
10654sketch(on = XY) {
10655  point(at = [var 1, var 2])
10656}
10657";
10658
10659        for (origin_first, snapshot_name) in [
10660            (true, "test_point_origin_coincident_preserves_order_origin_first"),
10661            (false, "test_point_origin_coincident_preserves_order_point_first"),
10662        ] {
10663            let program = Program::parse(initial_source).unwrap().0.unwrap();
10664
10665            let mut frontend = FrontendState::new();
10666
10667            let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10668            let mock_ctx = ExecutorContext::new_mock(None).await;
10669            let version = Version(0);
10670
10671            frontend.hack_set_program(&ctx, program).await.unwrap();
10672            let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10673            let sketch_id = sketch_object.id;
10674            let sketch = expect_sketch(sketch_object);
10675            let point_id = *sketch.segments.first().unwrap();
10676
10677            let segments = if origin_first {
10678                vec![ConstraintSegment::ORIGIN, point_id.into()]
10679            } else {
10680                vec![point_id.into(), ConstraintSegment::ORIGIN]
10681            };
10682            let constraint = Constraint::Coincident(Coincident {
10683                segments: segments.clone(),
10684            });
10685            let (src_delta, scene_delta) = frontend
10686                .add_constraint(&mock_ctx, version, sketch_id, constraint)
10687                .await
10688                .unwrap();
10689            insta::assert_snapshot!(snapshot_name, src_delta.text.as_str());
10690
10691            let constraint_object = scene_delta
10692                .new_graph
10693                .objects
10694                .iter()
10695                .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10696                .unwrap();
10697
10698            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10699                panic!("expected a constraint object");
10700            };
10701
10702            assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10703
10704            ctx.close().await;
10705            mock_ctx.close().await;
10706        }
10707    }
10708
10709    #[tokio::test(flavor = "multi_thread")]
10710    async fn test_coincident_of_line_end_points() {
10711        let initial_source = "\
10712sketch(on = XY) {
10713  line(start = [var 1, var 2], end = [var 3, var 4])
10714  line(start = [var 5, var 6], end = [var 7, var 8])
10715}
10716";
10717
10718        let program = Program::parse(initial_source).unwrap().0.unwrap();
10719
10720        let mut frontend = FrontendState::new();
10721
10722        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10723        let mock_ctx = ExecutorContext::new_mock(None).await;
10724        let version = Version(0);
10725
10726        frontend.hack_set_program(&ctx, program).await.unwrap();
10727        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10728        let sketch_id = sketch_object.id;
10729        let sketch = expect_sketch(sketch_object);
10730        let point0_id = *sketch.segments.get(1).unwrap();
10731        let point1_id = *sketch.segments.get(3).unwrap();
10732
10733        let constraint = Constraint::Coincident(Coincident {
10734            segments: vec![point0_id.into(), point1_id.into()],
10735        });
10736        let (src_delta, scene_delta) = frontend
10737            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10738            .await
10739            .unwrap();
10740        insta::assert_snapshot!("test_coincident_of_line_end_points", src_delta.text.as_str());
10741        assert_eq!(
10742            scene_delta.new_graph.objects.len(),
10743            9,
10744            "{:#?}",
10745            scene_delta.new_graph.objects
10746        );
10747
10748        ctx.close().await;
10749        mock_ctx.close().await;
10750    }
10751
10752    #[tokio::test(flavor = "multi_thread")]
10753    async fn test_coincident_of_line_point_and_circle_segment() {
10754        let initial_source = "\
10755sketch(on = XY) {
10756  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
10757  line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
10758}
10759";
10760        let program = Program::parse(initial_source).unwrap().0.unwrap();
10761        let mut frontend = FrontendState::new();
10762
10763        let mock_ctx = ExecutorContext::new_mock(None).await;
10764        let version = Version(0);
10765
10766        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10767        frontend.program = program;
10768        frontend.update_state_after_exec(outcome, true);
10769        let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
10770        let sketch_id = sketch_object.id;
10771        let sketch = expect_sketch(sketch_object);
10772
10773        let circle_id = sketch
10774            .segments
10775            .iter()
10776            .copied()
10777            .find(|seg_id| {
10778                matches!(
10779                    &frontend.scene_graph.objects[seg_id.0].kind,
10780                    ObjectKind::Segment {
10781                        segment: Segment::Circle(_)
10782                    }
10783                )
10784            })
10785            .expect("Expected a circle segment in sketch");
10786        let line_id = frontend
10787            .scene_graph
10788            .objects
10789            .iter()
10790            .find_map(|obj| match &obj.kind {
10791                ObjectKind::Segment {
10792                    segment: Segment::Line(line),
10793                } if line.owner.is_none() => Some(obj.id),
10794                _ => None,
10795            })
10796            .expect("Expected a standalone line segment in scene graph");
10797
10798        let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
10799            ObjectKind::Segment {
10800                segment: Segment::Line(line),
10801            } => line.start,
10802            _ => panic!("Expected line segment object"),
10803        };
10804
10805        let constraint = Constraint::Coincident(Coincident {
10806            segments: vec![line_start_point_id.into(), circle_id.into()],
10807        });
10808        let (src_delta, _scene_delta) = frontend
10809            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10810            .await
10811            .unwrap();
10812        insta::assert_snapshot!(
10813            "test_coincident_of_line_point_and_circle_segment",
10814            src_delta.text.as_str()
10815        );
10816
10817        mock_ctx.close().await;
10818    }
10819
10820    #[tokio::test(flavor = "multi_thread")]
10821    async fn test_invalid_coincident_arc_and_line_preserves_state() {
10822        // Test that attempting an invalid coincident constraint (arc and line)
10823        // doesn't corrupt the state, allowing subsequent operations to work.
10824        // This test verifies the transactional fix in add_constraint that prevents
10825        // state corruption when invalid constraints are attempted.
10826        // Example: coincident constraint between an arc segment and a straight line segment
10827        // is geometrically invalid and should fail, but state should remain intact.
10828        // Use the programmatic approach (new_sketch + add_segment) like test_new_sketch_add_arc_edit_arc
10829        let program = Program::empty();
10830
10831        let mut frontend = FrontendState::new();
10832        frontend.program = program;
10833
10834        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10835        let mock_ctx = ExecutorContext::new_mock(None).await;
10836        let version = Version(0);
10837
10838        let sketch_args = SketchCtor {
10839            on: Plane::Default(PlaneName::Xy),
10840        };
10841        let (_src_delta, _scene_delta, sketch_id) = frontend
10842            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10843            .await
10844            .unwrap();
10845
10846        // Add an arc segment
10847        let arc_ctor = ArcCtor {
10848            start: Point2d {
10849                x: Expr::Var(Number {
10850                    value: 0.0,
10851                    units: NumericSuffix::Mm,
10852                }),
10853                y: Expr::Var(Number {
10854                    value: 0.0,
10855                    units: NumericSuffix::Mm,
10856                }),
10857            },
10858            end: Point2d {
10859                x: Expr::Var(Number {
10860                    value: 10.0,
10861                    units: NumericSuffix::Mm,
10862                }),
10863                y: Expr::Var(Number {
10864                    value: 10.0,
10865                    units: NumericSuffix::Mm,
10866                }),
10867            },
10868            center: Point2d {
10869                x: Expr::Var(Number {
10870                    value: 10.0,
10871                    units: NumericSuffix::Mm,
10872                }),
10873                y: Expr::Var(Number {
10874                    value: 0.0,
10875                    units: NumericSuffix::Mm,
10876                }),
10877            },
10878            direction: None,
10879            construction: None,
10880        };
10881        let (_src_delta, scene_delta) = frontend
10882            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
10883            .await
10884            .unwrap();
10885        // The arc is the last object in new_objects (after the 3 points: start, end, center)
10886        let arc_id = *scene_delta.new_objects.last().unwrap();
10887
10888        // Add a line segment
10889        let line_ctor = LineCtor {
10890            start: Point2d {
10891                x: Expr::Var(Number {
10892                    value: 20.0,
10893                    units: NumericSuffix::Mm,
10894                }),
10895                y: Expr::Var(Number {
10896                    value: 0.0,
10897                    units: NumericSuffix::Mm,
10898                }),
10899            },
10900            end: Point2d {
10901                x: Expr::Var(Number {
10902                    value: 30.0,
10903                    units: NumericSuffix::Mm,
10904                }),
10905                y: Expr::Var(Number {
10906                    value: 10.0,
10907                    units: NumericSuffix::Mm,
10908                }),
10909            },
10910            construction: None,
10911        };
10912        let (_src_delta, scene_delta) = frontend
10913            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
10914            .await
10915            .unwrap();
10916        // The line is the last object in new_objects (after the 2 points: start, end)
10917        let line_id = *scene_delta.new_objects.last().unwrap();
10918
10919        // Attempt to add an invalid coincident constraint between arc and line
10920        // This should fail during execution, but state should remain intact
10921        let constraint = Constraint::Coincident(Coincident {
10922            segments: vec![arc_id.into(), line_id.into()],
10923        });
10924        let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
10925
10926        // The constraint addition should fail (invalid constraint)
10927        assert!(result.is_err(), "Expected invalid coincident constraint to fail");
10928
10929        // Verify state is not corrupted by checking that we can still access the scene graph
10930        // and that the original segments are still present with their source ranges
10931        let sketch_object_after =
10932            find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
10933        let sketch_after = expect_sketch(sketch_object_after);
10934
10935        // Verify both segments are still in the sketch
10936        assert!(
10937            sketch_after.segments.contains(&arc_id),
10938            "Arc segment should still exist after failed constraint"
10939        );
10940        assert!(
10941            sketch_after.segments.contains(&line_id),
10942            "Line segment should still exist after failed constraint"
10943        );
10944
10945        // Verify we can still access segment objects (this would fail if source ranges were corrupted)
10946        let arc_obj = frontend
10947            .scene_graph
10948            .objects
10949            .get(arc_id.0)
10950            .expect("Arc object should still be accessible");
10951        let line_obj = frontend
10952            .scene_graph
10953            .objects
10954            .get(line_id.0)
10955            .expect("Line object should still be accessible");
10956
10957        // Verify source ranges are still valid (not corrupted)
10958        // Just verify that the objects are still accessible and have the expected types
10959        match &arc_obj.kind {
10960            ObjectKind::Segment {
10961                segment: Segment::Arc(_),
10962            } => {}
10963            _ => panic!("Arc object should still be an arc segment"),
10964        }
10965        match &line_obj.kind {
10966            ObjectKind::Segment {
10967                segment: Segment::Line(_),
10968            } => {}
10969            _ => panic!("Line object should still be a line segment"),
10970        }
10971
10972        ctx.close().await;
10973        mock_ctx.close().await;
10974    }
10975
10976    #[tokio::test(flavor = "multi_thread")]
10977    async fn test_distance_two_points() {
10978        let initial_source = "\
10979sketch(on = XY) {
10980  point(at = [var 1, var 2])
10981  point(at = [var 3, var 4])
10982}
10983";
10984
10985        let program = Program::parse(initial_source).unwrap().0.unwrap();
10986
10987        let mut frontend = FrontendState::new();
10988
10989        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10990        let mock_ctx = ExecutorContext::new_mock(None).await;
10991        let version = Version(0);
10992
10993        frontend.hack_set_program(&ctx, program).await.unwrap();
10994        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10995        let sketch_id = sketch_object.id;
10996        let sketch = expect_sketch(sketch_object);
10997        let point0_id = *sketch.segments.first().unwrap();
10998        let point1_id = *sketch.segments.get(1).unwrap();
10999
11000        let constraint = Constraint::Distance(Distance {
11001            segments: vec![point0_id.into(), point1_id.into()],
11002            distance: Number {
11003                value: 2.0,
11004                units: NumericSuffix::Mm,
11005            },
11006            label_position: None,
11007            source: Default::default(),
11008        });
11009        let (src_delta, scene_delta) = frontend
11010            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11011            .await
11012            .unwrap();
11013        insta::assert_snapshot!("test_distance_two_points", src_delta.text.as_str());
11014        assert_eq!(
11015            scene_delta.new_graph.objects.len(),
11016            5,
11017            "{:#?}",
11018            scene_delta.new_graph.objects
11019        );
11020
11021        ctx.close().await;
11022        mock_ctx.close().await;
11023    }
11024
11025    #[tokio::test(flavor = "multi_thread")]
11026    async fn test_distance_two_points_with_label() {
11027        let initial_source = "\
11028sketch(on = XY) {
11029  point(at = [var 1, var 2])
11030  point(at = [var 3, var 4])
11031}
11032";
11033
11034        let program = Program::parse(initial_source).unwrap().0.unwrap();
11035
11036        let mut frontend = FrontendState::new();
11037
11038        let mock_ctx = ExecutorContext::new_mock(None).await;
11039        let version = Version(0);
11040
11041        frontend.program = program.clone();
11042        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11043        frontend.update_state_after_exec(outcome, true);
11044        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11045        let sketch_id = sketch_object.id;
11046        let sketch = expect_sketch(sketch_object);
11047        let point0_id = *sketch.segments.first().unwrap();
11048        let point1_id = *sketch.segments.get(1).unwrap();
11049
11050        let label_position = Point2d {
11051            x: Number {
11052                value: 10.0,
11053                units: NumericSuffix::Mm,
11054            },
11055            y: Number {
11056                value: 11.0,
11057                units: NumericSuffix::Mm,
11058            },
11059        };
11060        let constraint = Constraint::Distance(Distance {
11061            segments: vec![point0_id.into(), point1_id.into()],
11062            distance: Number {
11063                value: 2.0,
11064                units: NumericSuffix::Mm,
11065            },
11066            label_position: Some(label_position.clone()),
11067            source: Default::default(),
11068        });
11069        let (src_delta, scene_delta) = frontend
11070            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11071            .await
11072            .unwrap();
11073        insta::assert_snapshot!("test_distance_two_points_with_label", src_delta.text.as_str());
11074
11075        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11076        let sketch = expect_sketch(sketch_object);
11077        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11078        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11079            panic!("Expected constraint object");
11080        };
11081        let Constraint::Distance(distance) = constraint else {
11082            panic!("Expected distance constraint");
11083        };
11084        assert_eq!(distance.label_position, Some(label_position));
11085
11086        mock_ctx.close().await;
11087    }
11088
11089    #[tokio::test(flavor = "multi_thread")]
11090    async fn test_edit_distance_constraint_label_position() {
11091        let initial_source = "\
11092sketch(on = XY) {
11093  point(at = [var 1, var 2])
11094  point(at = [var 3, var 2])
11095}
11096";
11097
11098        let program = Program::parse(initial_source).unwrap().0.unwrap();
11099
11100        let mut frontend = FrontendState::new();
11101
11102        let mock_ctx = ExecutorContext::new_mock(None).await;
11103        let version = Version(0);
11104
11105        frontend.program = program.clone();
11106        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11107        frontend.update_state_after_exec(outcome, true);
11108        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11109        let sketch_id = sketch_object.id;
11110        let sketch = expect_sketch(sketch_object);
11111        let point0_id = *sketch.segments.first().unwrap();
11112        let point1_id = *sketch.segments.get(1).unwrap();
11113
11114        let constraint = Constraint::Distance(Distance {
11115            segments: vec![point0_id.into(), point1_id.into()],
11116            distance: Number {
11117                value: 2.0,
11118                units: NumericSuffix::Mm,
11119            },
11120            label_position: None,
11121            source: Default::default(),
11122        });
11123        let (_, scene_delta) = frontend
11124            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11125            .await
11126            .unwrap();
11127        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11128        let sketch = expect_sketch(sketch_object);
11129        let constraint_id = sketch.constraints[0];
11130        let label_position = Point2d {
11131            x: Number {
11132                value: 10.0,
11133                units: NumericSuffix::Mm,
11134            },
11135            y: Number {
11136                value: 11.0,
11137                units: NumericSuffix::Mm,
11138            },
11139        };
11140
11141        let (src_delta, scene_delta) = frontend
11142            .edit_distance_constraint_label_position(
11143                &mock_ctx,
11144                version,
11145                sketch_id,
11146                constraint_id,
11147                label_position.clone(),
11148                vec![],
11149            )
11150            .await
11151            .unwrap();
11152        insta::assert_snapshot!("test_edit_distance_constraint_label_position", src_delta.text.as_str());
11153
11154        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11155        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11156            panic!("Expected constraint object");
11157        };
11158        let Constraint::Distance(distance) = constraint else {
11159            panic!("Expected distance constraint");
11160        };
11161        assert_eq!(distance.label_position, Some(label_position));
11162
11163        mock_ctx.close().await;
11164    }
11165
11166    #[tokio::test(flavor = "multi_thread")]
11167    async fn test_edit_distance_constraint_type_and_value() {
11168        let initial_source = "\
11169sketch(on = XY) {
11170  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 3mm])
11171  distance([line1.start, line1.end]) == 5mm
11172}
11173";
11174
11175        let program = Program::parse(initial_source).unwrap().0.unwrap();
11176        let mut frontend = FrontendState::new();
11177        let mock_ctx = ExecutorContext::new_mock(None).await;
11178        let version = Version(0);
11179
11180        frontend.program = program.clone();
11181        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11182        frontend.update_state_after_exec(outcome, true);
11183        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11184        let sketch_id = sketch_object.id;
11185        let sketch = expect_sketch(sketch_object);
11186        let constraint_id = sketch.constraints[0];
11187        let point0_id = sketch.segments[0];
11188        let point1_id = sketch.segments[1];
11189        let label_position = Point2d {
11190            x: Number {
11191                value: 2.0,
11192                units: NumericSuffix::Mm,
11193            },
11194            y: Number {
11195                value: 5.0,
11196                units: NumericSuffix::Mm,
11197            },
11198        };
11199
11200        let (source_delta, scene_delta) = frontend
11201            .edit_distance_constraint_with_options(
11202                &mock_ctx,
11203                version,
11204                sketch_id,
11205                constraint_id,
11206                Constraint::HorizontalDistance(Distance {
11207                    segments: vec![point0_id.into(), point1_id.into()],
11208                    distance: Number {
11209                        value: 4.0,
11210                        units: NumericSuffix::Mm,
11211                    },
11212                    label_position: Some(label_position.clone()),
11213                    source: Default::default(),
11214                }),
11215                EditConstraintOptions {
11216                    commit_solved_initial_guesses: false,
11217                },
11218            )
11219            .await
11220            .unwrap();
11221        assert_eq!(
11222            source_delta.text,
11223            "\
11224sketch(on = XY) {
11225  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 3mm])
11226  horizontalDistance([line1.start, line1.end], labelPosition = [2mm, 5mm]) == 4mm
11227}
11228"
11229        );
11230
11231        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11232        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11233            panic!("Expected constraint object");
11234        };
11235        let Constraint::HorizontalDistance(distance) = constraint else {
11236            panic!("Expected horizontal distance constraint");
11237        };
11238        assert_eq!(distance.distance.value, 4.0);
11239        assert_eq!(distance.label_position, Some(label_position));
11240
11241        mock_ctx.close().await;
11242    }
11243
11244    #[tokio::test(flavor = "multi_thread")]
11245    async fn test_edit_angle_constraint_label_position() {
11246        let initial_source = "\
11247sketch(on = XY) {
11248  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11249  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11250  angle([line1, line2]) == 60deg
11251}
11252";
11253
11254        let program = Program::parse(initial_source).unwrap().0.unwrap();
11255        let mut frontend = FrontendState::new();
11256        let mock_ctx = ExecutorContext::new_mock(None).await;
11257        let version = Version(0);
11258
11259        frontend.program = program.clone();
11260        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11261        frontend.update_state_after_exec(outcome, true);
11262        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11263        let sketch_id = sketch_object.id;
11264        let sketch = expect_sketch(sketch_object);
11265        let constraint_id = sketch.constraints[0];
11266        let label_position = Point2d {
11267            x: Number {
11268                value: 10.0,
11269                units: NumericSuffix::Mm,
11270            },
11271            y: Number {
11272                value: 11.0,
11273                units: NumericSuffix::Mm,
11274            },
11275        };
11276
11277        let (src_delta, scene_delta) = frontend
11278            .edit_distance_constraint_label_position(
11279                &mock_ctx,
11280                version,
11281                sketch_id,
11282                constraint_id,
11283                label_position.clone(),
11284                vec![],
11285            )
11286            .await
11287            .unwrap();
11288        assert_eq!(
11289            src_delta.text.as_str(),
11290            "\
11291sketch(on = XY) {
11292  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11293  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.46mm])
11294  angle([line1, line2], labelPosition = [10mm, 11mm]) == 60deg
11295}
11296"
11297        );
11298
11299        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11300        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11301            panic!("Expected constraint object");
11302        };
11303        let Constraint::Angle(angle) = constraint else {
11304            panic!("Expected angle constraint");
11305        };
11306        assert_eq!(angle.label_position, Some(label_position));
11307
11308        mock_ctx.close().await;
11309    }
11310
11311    #[tokio::test(flavor = "multi_thread")]
11312    async fn test_edit_angle_constraint_label_position_with_call_on_right() {
11313        let initial_source = "\
11314sketch(on = XY) {
11315  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11316  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11317  60deg == angleDimension(lines = [line1, line2], sector = 1)
11318}
11319";
11320
11321        let program = Program::parse(initial_source).unwrap().0.unwrap();
11322        let mut frontend = FrontendState::new();
11323        let mock_ctx = ExecutorContext::new_mock(None).await;
11324        let version = Version(0);
11325
11326        frontend.program = program.clone();
11327        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11328        frontend.update_state_after_exec(outcome, true);
11329        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11330        let sketch_id = sketch_object.id;
11331        let sketch = expect_sketch(sketch_object);
11332        let constraint_id = sketch.constraints[0];
11333        let label_position = Point2d {
11334            x: Number {
11335                value: 10.0,
11336                units: NumericSuffix::Mm,
11337            },
11338            y: Number {
11339                value: 11.0,
11340                units: NumericSuffix::Mm,
11341            },
11342        };
11343
11344        let (src_delta, scene_delta) = frontend
11345            .edit_distance_constraint_label_position(
11346                &mock_ctx,
11347                version,
11348                sketch_id,
11349                constraint_id,
11350                label_position.clone(),
11351                vec![],
11352            )
11353            .await
11354            .unwrap();
11355        assert_eq!(
11356            src_delta.text.as_str(),
11357            "\
11358sketch(on = XY) {
11359  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11360  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.46mm])
11361  60deg == angleDimension(lines = [line1, line2], sector = 1, labelPosition = [10mm, 11mm])
11362}
11363"
11364        );
11365
11366        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11367        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11368            panic!("Expected constraint object");
11369        };
11370        let Constraint::Angle(angle) = constraint else {
11371            panic!("Expected angle constraint");
11372        };
11373        assert_eq!(angle.label_position, Some(label_position));
11374
11375        mock_ctx.close().await;
11376    }
11377
11378    #[tokio::test(flavor = "multi_thread")]
11379    async fn test_edit_angle_constraint() {
11380        let initial_source = "\
11381sketch(on = XY) {
11382  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11383  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11384  angle([line1, line2]) == 60deg
11385}
11386";
11387
11388        let program = Program::parse(initial_source).unwrap().0.unwrap();
11389        let mut frontend = FrontendState::new();
11390        let mock_ctx = ExecutorContext::new_mock(None).await;
11391        let version = Version(0);
11392
11393        frontend.program = program.clone();
11394        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11395        frontend.update_state_after_exec(outcome, true);
11396        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11397        let sketch_id = sketch_object.id;
11398        let sketch = expect_sketch(sketch_object);
11399        let constraint_id = sketch.constraints[0];
11400        let line1_id = *sketch.segments.get(2).unwrap();
11401        let line2_id = *sketch.segments.get(5).unwrap();
11402        let label_position = Point2d {
11403            x: Number {
11404                value: 10.0,
11405                units: NumericSuffix::Mm,
11406            },
11407            y: Number {
11408                value: 11.0,
11409                units: NumericSuffix::Mm,
11410            },
11411        };
11412
11413        let (src_delta, scene_delta) = frontend
11414            .edit_angle_constraint_with_options(
11415                &mock_ctx,
11416                version,
11417                sketch_id,
11418                constraint_id,
11419                Angle {
11420                    lines: vec![line2_id, line1_id],
11421                    angle: Number {
11422                        value: 60.0,
11423                        units: NumericSuffix::Deg,
11424                    },
11425                    sector: Some(3),
11426                    inverse: Some(false),
11427                    label_position: Some(label_position.clone()),
11428                    source: Default::default(),
11429                },
11430                EditConstraintOptions {
11431                    commit_solved_initial_guesses: false,
11432                },
11433            )
11434            .await
11435            .unwrap();
11436        assert_eq!(
11437            src_delta.text.as_str(),
11438            "\
11439sketch(on = XY) {
11440  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11441  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11442  angleDimension(lines = [line2, line1], sector = 3, labelPosition = [10mm, 11mm]) == 60deg
11443}
11444"
11445        );
11446
11447        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11448        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11449            panic!("Expected constraint object");
11450        };
11451        let Constraint::Angle(angle) = constraint else {
11452            panic!("Expected angle constraint");
11453        };
11454        assert_eq!(angle.lines, vec![line2_id, line1_id]);
11455        assert_eq!(angle.sector, Some(3));
11456        assert_eq!(angle.inverse, Some(false));
11457        assert_eq!(angle.label_position, Some(label_position));
11458
11459        mock_ctx.close().await;
11460    }
11461
11462    #[tokio::test(flavor = "multi_thread")]
11463    async fn test_edit_angle_constraint_with_call_on_right() {
11464        let initial_source = "\
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 == angle([line1, line2])
11469}
11470";
11471
11472        let program = Program::parse(initial_source).unwrap().0.unwrap();
11473        let mut frontend = FrontendState::new();
11474        let mock_ctx = ExecutorContext::new_mock(None).await;
11475        let version = Version(0);
11476
11477        frontend.program = program.clone();
11478        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11479        frontend.update_state_after_exec(outcome, true);
11480        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11481        let sketch_id = sketch_object.id;
11482        let sketch = expect_sketch(sketch_object);
11483        let constraint_id = sketch.constraints[0];
11484        let line1_id = *sketch.segments.get(2).unwrap();
11485        let line2_id = *sketch.segments.get(5).unwrap();
11486
11487        let (src_delta, _) = frontend
11488            .edit_angle_constraint_with_options(
11489                &mock_ctx,
11490                version,
11491                sketch_id,
11492                constraint_id,
11493                Angle {
11494                    lines: vec![line2_id, line1_id],
11495                    angle: Number {
11496                        value: 60.0,
11497                        units: NumericSuffix::Deg,
11498                    },
11499                    sector: Some(3),
11500                    inverse: Some(false),
11501                    label_position: None,
11502                    source: Default::default(),
11503                },
11504                EditConstraintOptions {
11505                    commit_solved_initial_guesses: false,
11506                },
11507            )
11508            .await
11509            .unwrap();
11510        assert_eq!(
11511            src_delta.text.as_str(),
11512            "\
11513sketch(on = XY) {
11514  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11515  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11516  60deg == angleDimension(lines = [line2, line1], sector = 3)
11517}
11518"
11519        );
11520
11521        mock_ctx.close().await;
11522    }
11523
11524    #[tokio::test(flavor = "multi_thread")]
11525    async fn test_edit_segments_can_commit_constraint_label_position_in_same_execution() {
11526        let initial_source = "\
11527@settings(kclVersion = 2.0)
11528
11529sketch001 = sketch(on = XZ) {
11530  line1 = line(start = [var 0mm, var 12.55mm], end = [var -6.03mm, var 8.51mm])
11531  line3 = line(start = [var -7.41mm, var 2.92mm], end = [var -1.47mm, var 4.32mm])
11532  distance([line1.start, line3.end], labelPosition = [5.56mm, 8.65mm]) == 8.36mm
11533  vertical([line1.start, ORIGIN])
11534}
11535";
11536
11537        let program = Program::parse(initial_source).unwrap().0.unwrap();
11538        let mut frontend = FrontendState::new();
11539        let mock_ctx = ExecutorContext::new_mock(None).await;
11540        let version = Version(0);
11541
11542        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
11543        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11544        let sketch_id = sketch_object.id;
11545        let sketch = expect_sketch(sketch_object);
11546        let constraint_id = sketch
11547            .constraints
11548            .iter()
11549            .copied()
11550            .find(|constraint_id| {
11551                matches!(
11552                    frontend.scene_graph.objects[constraint_id.0].kind,
11553                    ObjectKind::Constraint {
11554                        constraint: Constraint::Distance(_)
11555                    }
11556                )
11557            })
11558            .unwrap();
11559        let line1_id = sketch
11560            .segments
11561            .iter()
11562            .copied()
11563            .find(|segment_id| {
11564                matches!(
11565                    frontend.scene_graph.objects[segment_id.0].kind,
11566                    ObjectKind::Segment {
11567                        segment: Segment::Line(_)
11568                    }
11569                )
11570            })
11571            .unwrap();
11572        let label_position = Point2d {
11573            x: Number {
11574                value: 7.0,
11575                units: NumericSuffix::Mm,
11576            },
11577            y: Number {
11578                value: 9.0,
11579                units: NumericSuffix::Mm,
11580            },
11581        };
11582
11583        let (source_delta, scene_delta) = frontend
11584            .edit_segments_with_options(
11585                &mock_ctx,
11586                version,
11587                sketch_id,
11588                vec![ExistingSegmentCtor {
11589                    id: line1_id,
11590                    ctor: SegmentCtor::Line(LineCtor {
11591                        start: point_expr_mm(2.0, 15.55),
11592                        end: point_expr_mm(-4.03, 11.51),
11593                        construction: None,
11594                    }),
11595                }],
11596                EditSegmentsOptions {
11597                    anchor_segment_ids: Some(vec![]),
11598                    drag_anchors: vec![SegmentDragAnchor {
11599                        segment_id: line1_id,
11600                        target: label_position.clone(),
11601                    }],
11602                    constraint_label_edits: vec![ConstraintLabelPositionEdit {
11603                        constraint_id,
11604                        label_position: label_position.clone(),
11605                    }],
11606                    commit_solved_initial_guesses: true,
11607                },
11608            )
11609            .await
11610            .unwrap();
11611
11612        assert!(source_delta.text.contains("labelPosition = [7mm, 9mm]"));
11613        let constraint_object = &scene_delta.new_graph.objects[constraint_id.0];
11614        let ObjectKind::Constraint {
11615            constraint: Constraint::Distance(distance),
11616        } = &constraint_object.kind
11617        else {
11618            panic!("Expected distance constraint object");
11619        };
11620        assert_eq!(distance.label_position, Some(label_position));
11621
11622        let snapped_label_position = Point2d {
11623            x: Number {
11624                value: 8.0,
11625                units: NumericSuffix::Mm,
11626            },
11627            y: Number {
11628                value: 10.0,
11629                units: NumericSuffix::Mm,
11630            },
11631        };
11632        let (source_delta, scene_delta) = frontend
11633            .edit_segments_with_options(
11634                &mock_ctx,
11635                version,
11636                sketch_id,
11637                vec![],
11638                EditSegmentsOptions {
11639                    anchor_segment_ids: Some(vec![line1_id]),
11640                    drag_anchors: vec![],
11641                    constraint_label_edits: vec![ConstraintLabelPositionEdit {
11642                        constraint_id,
11643                        label_position: snapped_label_position.clone(),
11644                    }],
11645                    commit_solved_initial_guesses: true,
11646                },
11647            )
11648            .await
11649            .unwrap();
11650
11651        assert!(source_delta.text.contains("labelPosition = [8mm, 10mm]"));
11652        let constraint_object = &scene_delta.new_graph.objects[constraint_id.0];
11653        let ObjectKind::Constraint {
11654            constraint: Constraint::Distance(distance),
11655        } = &constraint_object.kind
11656        else {
11657            panic!("Expected distance constraint object");
11658        };
11659        assert_eq!(distance.label_position, Some(snapped_label_position));
11660
11661        mock_ctx.close().await;
11662    }
11663
11664    #[tokio::test(flavor = "multi_thread")]
11665    async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
11666        let initial_source = "\
11667sketch(on = XY) {
11668  point1 = point(at = [var 0mm, var 0mm])
11669  point2 = point(at = [var 10mm, var 0mm])
11670  distance([point1, point2]) == 5mm
11671}
11672";
11673
11674        let program = Program::parse(initial_source).unwrap().0.unwrap();
11675        let mut frontend = FrontendState::new();
11676        let mock_ctx = ExecutorContext::new_mock(None).await;
11677        let version = Version(0);
11678
11679        frontend.program = program.clone();
11680        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11681        frontend.update_state_after_exec(outcome, true);
11682        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11683        let sketch_id = sketch_object.id;
11684        let sketch = expect_sketch(sketch_object);
11685        let point0_id = sketch.segments[0];
11686        let point1_id = sketch.segments[1];
11687        let constraint_id = sketch.constraints[0];
11688
11689        let edited_segments = vec![ExistingSegmentCtor {
11690            id: point0_id,
11691            ctor: SegmentCtor::Point(PointCtor {
11692                position: Point2d {
11693                    x: Expr::Var(Number {
11694                        value: 2.0,
11695                        units: NumericSuffix::Mm,
11696                    }),
11697                    y: Expr::Var(Number {
11698                        value: 1.0,
11699                        units: NumericSuffix::Mm,
11700                    }),
11701                },
11702            }),
11703        }];
11704        let (_, scene_delta) = frontend
11705            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
11706            .await
11707            .unwrap();
11708        let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
11709        let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
11710
11711        let label_position = Point2d {
11712            x: Number {
11713                value: 3.0,
11714                units: NumericSuffix::Mm,
11715            },
11716            y: Number {
11717                value: 4.0,
11718                units: NumericSuffix::Mm,
11719            },
11720        };
11721        let (_, scene_delta) = frontend
11722            .edit_distance_constraint_label_position(
11723                &mock_ctx,
11724                version,
11725                sketch_id,
11726                constraint_id,
11727                label_position,
11728                vec![point0_id],
11729            )
11730            .await
11731            .unwrap();
11732
11733        assert_point_position_close(
11734            point_position(&scene_delta.new_graph, point0_id),
11735            point0_after_segment_edit,
11736        );
11737        assert_point_position_close(
11738            point_position(&scene_delta.new_graph, point1_id),
11739            point1_after_segment_edit,
11740        );
11741
11742        mock_ctx.close().await;
11743    }
11744
11745    #[tokio::test(flavor = "multi_thread")]
11746    async fn test_distance_point_line() {
11747        let initial_source = "\
11748sketch(on = XY) {
11749  point(at = [var 0, var 5])
11750  line(start = [var 0, var 0], end = [var 10, var 0])
11751}
11752";
11753
11754        let program = Program::parse(initial_source).unwrap().0.unwrap();
11755
11756        let mut frontend = FrontendState::new();
11757
11758        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11759        let mock_ctx = ExecutorContext::new_mock(None).await;
11760        let version = Version(0);
11761
11762        frontend.hack_set_program(&ctx, program).await.unwrap();
11763        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11764        let sketch_id = sketch_object.id;
11765        let sketch = expect_sketch(sketch_object);
11766        let point_id = *sketch.segments.first().unwrap();
11767        let line_id = *sketch
11768            .segments
11769            .iter()
11770            .find(|segment_id| {
11771                matches!(
11772                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11773                    Some(ObjectKind::Segment {
11774                        segment: Segment::Line(_)
11775                    })
11776                )
11777            })
11778            .unwrap();
11779
11780        let label_position = Point2d {
11781            x: Number {
11782                value: 10.0,
11783                units: NumericSuffix::Mm,
11784            },
11785            y: Number {
11786                value: 11.0,
11787                units: NumericSuffix::Mm,
11788            },
11789        };
11790        let constraint = Constraint::Distance(Distance {
11791            segments: vec![point_id.into(), line_id.into()],
11792            distance: Number {
11793                value: 5.0,
11794                units: NumericSuffix::Mm,
11795            },
11796            label_position: Some(label_position.clone()),
11797            source: Default::default(),
11798        });
11799        let (src_delta, scene_delta) = frontend
11800            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11801            .await
11802            .unwrap();
11803        insta::assert_snapshot!("test_distance_point_line", src_delta.text.as_str());
11804        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11805        let sketch = expect_sketch(sketch_object);
11806        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11807        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11808            panic!("Expected constraint object");
11809        };
11810        let Constraint::Distance(distance) = constraint else {
11811            panic!("Expected distance constraint");
11812        };
11813        assert_eq!(distance.label_position, Some(label_position));
11814
11815        ctx.close().await;
11816        mock_ctx.close().await;
11817    }
11818
11819    #[tokio::test(flavor = "multi_thread")]
11820    async fn test_distance_point_arc() {
11821        let initial_source = "\
11822sketch(on = XY) {
11823  point(at = [var 0, var 8])
11824  arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
11825}
11826";
11827
11828        let program = Program::parse(initial_source).unwrap().0.unwrap();
11829
11830        let mut frontend = FrontendState::new();
11831
11832        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11833        let mock_ctx = ExecutorContext::new_mock(None).await;
11834        let version = Version(0);
11835
11836        frontend.hack_set_program(&ctx, program).await.unwrap();
11837        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11838        let sketch_id = sketch_object.id;
11839        let sketch = expect_sketch(sketch_object);
11840        let point_id = *sketch.segments.first().unwrap();
11841        let arc_id = *sketch
11842            .segments
11843            .iter()
11844            .find(|segment_id| {
11845                matches!(
11846                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11847                    Some(ObjectKind::Segment {
11848                        segment: Segment::Arc(_)
11849                    })
11850                )
11851            })
11852            .unwrap();
11853
11854        let constraint = Constraint::Distance(Distance {
11855            segments: vec![point_id.into(), arc_id.into()],
11856            distance: Number {
11857                value: 3.0,
11858                units: NumericSuffix::Mm,
11859            },
11860            label_position: None,
11861            source: Default::default(),
11862        });
11863        let (src_delta, _scene_delta) = frontend
11864            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11865            .await
11866            .unwrap();
11867        insta::assert_snapshot!("test_distance_point_arc", src_delta.text.as_str());
11868
11869        ctx.close().await;
11870        mock_ctx.close().await;
11871    }
11872
11873    #[tokio::test(flavor = "multi_thread")]
11874    async fn test_distance_arc_origin() {
11875        let initial_source = "\
11876sketch001 = sketch(on = XY) {
11877  arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
11878}
11879";
11880
11881        let program = Program::parse(initial_source).unwrap().0.unwrap();
11882
11883        let mut frontend = FrontendState::new();
11884
11885        let mock_ctx = ExecutorContext::new_mock(None).await;
11886        let version = Version(0);
11887
11888        frontend.program = program.clone();
11889        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11890        frontend.update_state_after_exec(outcome, true);
11891        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11892        let sketch_id = sketch_object.id;
11893        let sketch = expect_sketch(sketch_object);
11894        let arc_id = *sketch
11895            .segments
11896            .iter()
11897            .find(|segment_id| {
11898                matches!(
11899                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11900                    Some(ObjectKind::Segment {
11901                        segment: Segment::Arc(_)
11902                    })
11903                )
11904            })
11905            .unwrap();
11906
11907        let constraint = Constraint::Distance(Distance {
11908            segments: vec![arc_id.into(), ConstraintSegment::ORIGIN],
11909            distance: Number {
11910                value: 3.0,
11911                units: NumericSuffix::Mm,
11912            },
11913            label_position: None,
11914            source: Default::default(),
11915        });
11916        let (src_delta, _scene_delta) = frontend
11917            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11918            .await
11919            .unwrap();
11920        insta::assert_snapshot!("test_distance_arc_origin", src_delta.text.as_str());
11921
11922        mock_ctx.close().await;
11923    }
11924
11925    #[tokio::test(flavor = "multi_thread")]
11926    async fn test_distance_line_origin() {
11927        let initial_source = "\
11928sketch(on = XY) {
11929  line(start = [var 5, var 0], end = [var 5, var 10])
11930}
11931";
11932
11933        let program = Program::parse(initial_source).unwrap().0.unwrap();
11934
11935        let mut frontend = FrontendState::new();
11936
11937        let mock_ctx = ExecutorContext::new_mock(None).await;
11938        let version = Version(0);
11939
11940        frontend.program = program.clone();
11941        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11942        frontend.update_state_after_exec(outcome, true);
11943        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11944        let sketch_id = sketch_object.id;
11945        let sketch = expect_sketch(sketch_object);
11946        let line_id = *sketch
11947            .segments
11948            .iter()
11949            .find(|segment_id| {
11950                matches!(
11951                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11952                    Some(ObjectKind::Segment {
11953                        segment: Segment::Line(_)
11954                    })
11955                )
11956            })
11957            .unwrap();
11958
11959        let constraint = Constraint::Distance(Distance {
11960            segments: vec![ConstraintSegment::ORIGIN, line_id.into()],
11961            distance: Number {
11962                value: 5.0,
11963                units: NumericSuffix::Mm,
11964            },
11965            label_position: None,
11966            source: Default::default(),
11967        });
11968        let (src_delta, _scene_delta) = frontend
11969            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11970            .await
11971            .unwrap();
11972        insta::assert_snapshot!("test_distance_line_origin", src_delta.text.as_str());
11973
11974        mock_ctx.close().await;
11975    }
11976
11977    #[tokio::test(flavor = "multi_thread")]
11978    async fn test_distance_line_circle() {
11979        let initial_source = "\
11980sketch(on = XY) {
11981  line(start = [var -10, var 8], end = [var 10, var 8])
11982  circle(start = [var 5, var 0], center = [var 0, var 0])
11983}
11984";
11985
11986        let program = Program::parse(initial_source).unwrap().0.unwrap();
11987
11988        let mut frontend = FrontendState::new();
11989
11990        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11991        let mock_ctx = ExecutorContext::new_mock(None).await;
11992        let version = Version(0);
11993
11994        frontend.hack_set_program(&ctx, program).await.unwrap();
11995        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11996        let sketch_id = sketch_object.id;
11997        let sketch = expect_sketch(sketch_object);
11998        let line_id = *sketch
11999            .segments
12000            .iter()
12001            .find(|segment_id| {
12002                matches!(
12003                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12004                    Some(ObjectKind::Segment {
12005                        segment: Segment::Line(_)
12006                    })
12007                )
12008            })
12009            .unwrap();
12010        let circle_id = *sketch
12011            .segments
12012            .iter()
12013            .find(|segment_id| {
12014                matches!(
12015                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12016                    Some(ObjectKind::Segment {
12017                        segment: Segment::Circle(_)
12018                    })
12019                )
12020            })
12021            .unwrap();
12022
12023        let constraint = Constraint::Distance(Distance {
12024            segments: vec![line_id.into(), circle_id.into()],
12025            distance: Number {
12026                value: 3.0,
12027                units: NumericSuffix::Mm,
12028            },
12029            label_position: None,
12030            source: Default::default(),
12031        });
12032        let (src_delta, _scene_delta) = frontend
12033            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12034            .await
12035            .unwrap();
12036        insta::assert_snapshot!("test_distance_line_circle", src_delta.text.as_str());
12037
12038        ctx.close().await;
12039        mock_ctx.close().await;
12040    }
12041
12042    #[tokio::test(flavor = "multi_thread")]
12043    async fn test_distance_circle_arc() {
12044        let initial_source = "\
12045sketch(on = XY) {
12046  circle(start = [var 5, var 0], center = [var 0, var 0])
12047  arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
12048}
12049";
12050
12051        let program = Program::parse(initial_source).unwrap().0.unwrap();
12052
12053        let mut frontend = FrontendState::new();
12054
12055        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12056        let mock_ctx = ExecutorContext::new_mock(None).await;
12057        let version = Version(0);
12058
12059        frontend.hack_set_program(&ctx, program).await.unwrap();
12060        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12061        let sketch_id = sketch_object.id;
12062        let sketch = expect_sketch(sketch_object);
12063        let circle_id = *sketch
12064            .segments
12065            .iter()
12066            .find(|segment_id| {
12067                matches!(
12068                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12069                    Some(ObjectKind::Segment {
12070                        segment: Segment::Circle(_)
12071                    })
12072                )
12073            })
12074            .unwrap();
12075        let arc_id = *sketch
12076            .segments
12077            .iter()
12078            .find(|segment_id| {
12079                matches!(
12080                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12081                    Some(ObjectKind::Segment {
12082                        segment: Segment::Arc(_)
12083                    })
12084                )
12085            })
12086            .unwrap();
12087
12088        let constraint = Constraint::Distance(Distance {
12089            segments: vec![circle_id.into(), arc_id.into()],
12090            distance: Number {
12091                value: 3.0,
12092                units: NumericSuffix::Mm,
12093            },
12094            label_position: None,
12095            source: Default::default(),
12096        });
12097        let (src_delta, _scene_delta) = frontend
12098            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12099            .await
12100            .unwrap();
12101        insta::assert_snapshot!("test_distance_circle_arc", src_delta.text.as_str());
12102
12103        ctx.close().await;
12104        mock_ctx.close().await;
12105    }
12106
12107    #[tokio::test(flavor = "multi_thread")]
12108    async fn test_distance_parallel_lines() {
12109        let initial_source = "\
12110sketch(on = XY) {
12111  line(start = [var 0, var 0], end = [var 10, var 0])
12112  line(start = [var 0, var 5], end = [var 10, var 5])
12113}
12114";
12115
12116        let program = Program::parse(initial_source).unwrap().0.unwrap();
12117
12118        let mut frontend = FrontendState::new();
12119
12120        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12121        let mock_ctx = ExecutorContext::new_mock(None).await;
12122        let version = Version(0);
12123
12124        frontend.hack_set_program(&ctx, program).await.unwrap();
12125        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12126        let sketch_id = sketch_object.id;
12127        let sketch = expect_sketch(sketch_object);
12128        let line_ids = sketch
12129            .segments
12130            .iter()
12131            .copied()
12132            .filter(|segment_id| {
12133                matches!(
12134                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12135                    Some(ObjectKind::Segment {
12136                        segment: Segment::Line(_)
12137                    })
12138                )
12139            })
12140            .collect::<Vec<_>>();
12141
12142        let constraint = Constraint::Distance(Distance {
12143            segments: vec![line_ids[0].into(), line_ids[1].into()],
12144            distance: Number {
12145                value: 5.0,
12146                units: NumericSuffix::Mm,
12147            },
12148            label_position: None,
12149            source: Default::default(),
12150        });
12151        let (src_delta, _scene_delta) = frontend
12152            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12153            .await
12154            .unwrap();
12155        insta::assert_snapshot!("test_distance_parallel_lines", src_delta.text.as_str());
12156
12157        ctx.close().await;
12158        mock_ctx.close().await;
12159    }
12160
12161    #[tokio::test(flavor = "multi_thread")]
12162    async fn test_distance_non_parallel_lines_lowers_to_distance() {
12163        // NOTE: Current LinesAtAngle constraint collapses if lines are initialized perpendicular to
12164        // one another because the gradient of the residual has no tangential component in this
12165        // configuration. The only path to reducing the residual is shrinking the lengths of the
12166        // lines which are causing the lines to collapse, producing a degenerate output.
12167        let initial_source = "\
12168sketch(on = XY) {
12169  line(start = [var 0, var 0], end = [var 10, var 0])
12170  line(start = [var 0, var 0], end = [var 10, var 10])
12171}
12172";
12173
12174        let program = Program::parse(initial_source).unwrap().0.unwrap();
12175
12176        let mut frontend = FrontendState::new();
12177
12178        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12179        let mock_ctx = ExecutorContext::new_mock(None).await;
12180        let version = Version(0);
12181
12182        frontend.hack_set_program(&ctx, program).await.unwrap();
12183        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12184        let sketch_id = sketch_object.id;
12185        let sketch = expect_sketch(sketch_object);
12186        let line_ids = sketch
12187            .segments
12188            .iter()
12189            .copied()
12190            .filter(|segment_id| {
12191                matches!(
12192                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12193                    Some(ObjectKind::Segment {
12194                        segment: Segment::Line(_)
12195                    })
12196                )
12197            })
12198            .collect::<Vec<_>>();
12199
12200        let constraint = Constraint::Distance(Distance {
12201            segments: vec![line_ids[0].into(), line_ids[1].into()],
12202            distance: Number {
12203                value: 5.0,
12204                units: NumericSuffix::Mm,
12205            },
12206            label_position: None,
12207            source: Default::default(),
12208        });
12209        let (src_delta, _scene_delta) = frontend
12210            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12211            .await
12212            .unwrap();
12213        insta::assert_snapshot!(
12214            "test_distance_non_parallel_lines_lowers_to_distance",
12215            src_delta.text.as_str()
12216        );
12217
12218        ctx.close().await;
12219        mock_ctx.close().await;
12220    }
12221
12222    #[tokio::test(flavor = "multi_thread")]
12223    async fn test_horizontal_distance_two_points() {
12224        let initial_source = "\
12225sketch(on = XY) {
12226  point(at = [var 1, var 2])
12227  point(at = [var 3, var 4])
12228}
12229";
12230
12231        let program = Program::parse(initial_source).unwrap().0.unwrap();
12232
12233        let mut frontend = FrontendState::new();
12234
12235        let mock_ctx = ExecutorContext::new_mock(None).await;
12236        let version = Version(0);
12237
12238        frontend.program = program.clone();
12239        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12240        frontend.update_state_after_exec(outcome, true);
12241        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12242        let sketch_id = sketch_object.id;
12243        let sketch = expect_sketch(sketch_object);
12244        let point0_id = *sketch.segments.first().unwrap();
12245        let point1_id = *sketch.segments.get(1).unwrap();
12246        let label_position = Point2d {
12247            x: Number {
12248                value: 10.0,
12249                units: NumericSuffix::Mm,
12250            },
12251            y: Number {
12252                value: 11.0,
12253                units: NumericSuffix::Mm,
12254            },
12255        };
12256
12257        let constraint = Constraint::HorizontalDistance(Distance {
12258            segments: vec![point0_id.into(), point1_id.into()],
12259            distance: Number {
12260                value: 2.0,
12261                units: NumericSuffix::Mm,
12262            },
12263            label_position: Some(label_position.clone()),
12264            source: Default::default(),
12265        });
12266        let (src_delta, scene_delta) = frontend
12267            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12268            .await
12269            .unwrap();
12270        insta::assert_snapshot!("test_horizontal_distance_two_points", src_delta.text.as_str());
12271        assert_eq!(
12272            scene_delta.new_graph.objects.len(),
12273            5,
12274            "{:#?}",
12275            scene_delta.new_graph.objects
12276        );
12277        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12278        let sketch = expect_sketch(sketch_object);
12279        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12280        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12281            panic!("Expected constraint object");
12282        };
12283        let Constraint::HorizontalDistance(distance) = constraint else {
12284            panic!("Expected horizontal distance constraint");
12285        };
12286        assert_eq!(distance.label_position, Some(label_position));
12287
12288        mock_ctx.close().await;
12289    }
12290
12291    #[tokio::test(flavor = "multi_thread")]
12292    async fn test_radius_single_arc_segment() {
12293        let initial_source = "\
12294sketch(on = XY) {
12295  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12296}
12297";
12298
12299        let program = Program::parse(initial_source).unwrap().0.unwrap();
12300
12301        let mut frontend = FrontendState::new();
12302
12303        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12304        let mock_ctx = ExecutorContext::new_mock(None).await;
12305        let version = Version(0);
12306
12307        frontend.hack_set_program(&ctx, program).await.unwrap();
12308        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12309        let sketch_id = sketch_object.id;
12310        let sketch = expect_sketch(sketch_object);
12311        // Find the arc segment (not the points)
12312        let arc_id = sketch
12313            .segments
12314            .iter()
12315            .find(|&seg_id| {
12316                let obj = frontend.scene_graph.objects.get(seg_id.0);
12317                matches!(
12318                    obj.map(|o| &o.kind),
12319                    Some(ObjectKind::Segment {
12320                        segment: Segment::Arc(_)
12321                    })
12322                )
12323            })
12324            .unwrap();
12325
12326        let constraint = Constraint::Radius(Radius {
12327            arc: *arc_id,
12328            radius: Number {
12329                value: 5.0,
12330                units: NumericSuffix::Mm,
12331            },
12332            label_position: None,
12333            source: Default::default(),
12334        });
12335        let (src_delta, scene_delta) = frontend
12336            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12337            .await
12338            .unwrap();
12339        insta::assert_snapshot!("test_radius_single_arc_segment", src_delta.text.as_str());
12340        assert_eq!(
12341            scene_delta.new_graph.objects.len(),
12342            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
12343            "{:#?}",
12344            scene_delta.new_graph.objects
12345        );
12346
12347        ctx.close().await;
12348        mock_ctx.close().await;
12349    }
12350
12351    #[tokio::test(flavor = "multi_thread")]
12352    async fn test_radius_single_arc_segment_with_label_position() {
12353        let initial_source = "\
12354sketch(on = XY) {
12355  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12356}
12357";
12358
12359        let program = Program::parse(initial_source).unwrap().0.unwrap();
12360        let mut frontend = FrontendState::new();
12361        let mock_ctx = ExecutorContext::new_mock(None).await;
12362        let version = Version(0);
12363
12364        frontend.program = program.clone();
12365        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12366        frontend.update_state_after_exec(outcome, true);
12367        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12368        let sketch_id = sketch_object.id;
12369        let sketch = expect_sketch(sketch_object);
12370        let arc_id = sketch
12371            .segments
12372            .iter()
12373            .find(|&seg_id| {
12374                let obj = frontend.scene_graph.objects.get(seg_id.0);
12375                matches!(
12376                    obj.map(|o| &o.kind),
12377                    Some(ObjectKind::Segment {
12378                        segment: Segment::Arc(_)
12379                    })
12380                )
12381            })
12382            .unwrap();
12383
12384        let label_position = Point2d {
12385            x: Number {
12386                value: 10.0,
12387                units: NumericSuffix::Mm,
12388            },
12389            y: Number {
12390                value: 11.0,
12391                units: NumericSuffix::Mm,
12392            },
12393        };
12394        let constraint = Constraint::Radius(Radius {
12395            arc: *arc_id,
12396            radius: Number {
12397                value: 5.0,
12398                units: NumericSuffix::Mm,
12399            },
12400            label_position: Some(label_position.clone()),
12401            source: Default::default(),
12402        });
12403        let (src_delta, scene_delta) = frontend
12404            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12405            .await
12406            .unwrap();
12407        insta::assert_snapshot!(
12408            "test_radius_single_arc_segment_with_label_position",
12409            src_delta.text.as_str()
12410        );
12411
12412        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12413        let sketch = expect_sketch(sketch_object);
12414        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12415        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12416            panic!("Expected constraint object");
12417        };
12418        let Constraint::Radius(radius) = constraint else {
12419            panic!("Expected radius constraint");
12420        };
12421        assert_eq!(radius.label_position, Some(label_position));
12422
12423        mock_ctx.close().await;
12424    }
12425
12426    #[tokio::test(flavor = "multi_thread")]
12427    async fn test_edit_radius_constraint_label_position() {
12428        let initial_source = "\
12429sketch(on = XY) {
12430  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12431  radius(arc1) == 5mm
12432}
12433";
12434
12435        let program = Program::parse(initial_source).unwrap().0.unwrap();
12436        let mut frontend = FrontendState::new();
12437        let mock_ctx = ExecutorContext::new_mock(None).await;
12438        let version = Version(0);
12439
12440        frontend.program = program.clone();
12441        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12442        frontend.update_state_after_exec(outcome, true);
12443        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12444        let sketch_id = sketch_object.id;
12445        let sketch = expect_sketch(sketch_object);
12446        let constraint_id = sketch.constraints[0];
12447        let label_position = Point2d {
12448            x: Number {
12449                value: 10.0,
12450                units: NumericSuffix::Mm,
12451            },
12452            y: Number {
12453                value: 11.0,
12454                units: NumericSuffix::Mm,
12455            },
12456        };
12457
12458        let (src_delta, scene_delta) = frontend
12459            .edit_distance_constraint_label_position(
12460                &mock_ctx,
12461                version,
12462                sketch_id,
12463                constraint_id,
12464                label_position.clone(),
12465                vec![],
12466            )
12467            .await
12468            .unwrap();
12469        insta::assert_snapshot!("test_edit_radius_constraint_label_position", src_delta.text.as_str());
12470
12471        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12472        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12473            panic!("Expected constraint object");
12474        };
12475        let Constraint::Radius(radius) = constraint else {
12476            panic!("Expected radius constraint");
12477        };
12478        assert_eq!(radius.label_position, Some(label_position));
12479
12480        mock_ctx.close().await;
12481    }
12482
12483    #[tokio::test(flavor = "multi_thread")]
12484    async fn test_vertical_distance_two_points() {
12485        let initial_source = "\
12486sketch(on = XY) {
12487  point(at = [var 1, var 2])
12488  point(at = [var 3, var 4])
12489}
12490";
12491
12492        let program = Program::parse(initial_source).unwrap().0.unwrap();
12493
12494        let mut frontend = FrontendState::new();
12495
12496        let mock_ctx = ExecutorContext::new_mock(None).await;
12497        let version = Version(0);
12498
12499        frontend.program = program.clone();
12500        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12501        frontend.update_state_after_exec(outcome, true);
12502        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12503        let sketch_id = sketch_object.id;
12504        let sketch = expect_sketch(sketch_object);
12505        let point0_id = *sketch.segments.first().unwrap();
12506        let point1_id = *sketch.segments.get(1).unwrap();
12507        let label_position = Point2d {
12508            x: Number {
12509                value: 10.0,
12510                units: NumericSuffix::Mm,
12511            },
12512            y: Number {
12513                value: 11.0,
12514                units: NumericSuffix::Mm,
12515            },
12516        };
12517
12518        let constraint = Constraint::VerticalDistance(Distance {
12519            segments: vec![point0_id.into(), point1_id.into()],
12520            distance: Number {
12521                value: 2.0,
12522                units: NumericSuffix::Mm,
12523            },
12524            label_position: Some(label_position.clone()),
12525            source: Default::default(),
12526        });
12527        let (src_delta, scene_delta) = frontend
12528            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12529            .await
12530            .unwrap();
12531        insta::assert_snapshot!("test_vertical_distance_two_points", src_delta.text.as_str());
12532        assert_eq!(
12533            scene_delta.new_graph.objects.len(),
12534            5,
12535            "{:#?}",
12536            scene_delta.new_graph.objects
12537        );
12538        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12539        let sketch = expect_sketch(sketch_object);
12540        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12541        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12542            panic!("Expected constraint object");
12543        };
12544        let Constraint::VerticalDistance(distance) = constraint else {
12545            panic!("Expected vertical distance constraint");
12546        };
12547        assert_eq!(distance.label_position, Some(label_position));
12548
12549        mock_ctx.close().await;
12550    }
12551
12552    #[tokio::test(flavor = "multi_thread")]
12553    async fn test_add_fixed_standalone_point() {
12554        let initial_source = "\
12555sketch(on = XY) {
12556  point(at = [var 1, var 2])
12557}
12558";
12559
12560        let program = Program::parse(initial_source).unwrap().0.unwrap();
12561
12562        let mut frontend = FrontendState::new();
12563
12564        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12565        let mock_ctx = ExecutorContext::new_mock(None).await;
12566        let version = Version(0);
12567
12568        frontend.hack_set_program(&ctx, program).await.unwrap();
12569        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12570        let sketch_id = sketch_object.id;
12571        let sketch = expect_sketch(sketch_object);
12572        let point_id = *sketch.segments.first().unwrap();
12573
12574        let (src_delta, scene_delta) = frontend
12575            .add_constraint(
12576                &mock_ctx,
12577                version,
12578                sketch_id,
12579                Constraint::Fixed(Fixed {
12580                    points: vec![FixedPoint {
12581                        point: point_id,
12582                        position: Point2d {
12583                            x: Number {
12584                                value: 2.0,
12585                                units: NumericSuffix::Mm,
12586                            },
12587                            y: Number {
12588                                value: 3.0,
12589                                units: NumericSuffix::Mm,
12590                            },
12591                        },
12592                    }],
12593                }),
12594            )
12595            .await
12596            .unwrap();
12597        insta::assert_snapshot!("test_add_fixed_standalone_point", src_delta.text.as_str());
12598        assert_eq!(
12599            scene_delta.new_graph.objects.len(),
12600            4,
12601            "{:#?}",
12602            scene_delta.new_graph.objects
12603        );
12604
12605        ctx.close().await;
12606        mock_ctx.close().await;
12607    }
12608
12609    #[tokio::test(flavor = "multi_thread")]
12610    async fn test_add_fixed_multiple_points() {
12611        let initial_source = "\
12612sketch(on = XY) {
12613  point(at = [var 1, var 2])
12614  point(at = [var 3, var 4])
12615}
12616";
12617
12618        let program = Program::parse(initial_source).unwrap().0.unwrap();
12619
12620        let mut frontend = FrontendState::new();
12621
12622        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12623        let mock_ctx = ExecutorContext::new_mock(None).await;
12624        let version = Version(0);
12625
12626        frontend.hack_set_program(&ctx, program).await.unwrap();
12627        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12628        let sketch_id = sketch_object.id;
12629        let sketch = expect_sketch(sketch_object);
12630        let point0_id = *sketch.segments.first().unwrap();
12631        let point1_id = *sketch.segments.get(1).unwrap();
12632
12633        let (src_delta, scene_delta) = frontend
12634            .add_constraint(
12635                &mock_ctx,
12636                version,
12637                sketch_id,
12638                Constraint::Fixed(Fixed {
12639                    points: vec![
12640                        FixedPoint {
12641                            point: point0_id,
12642                            position: Point2d {
12643                                x: Number {
12644                                    value: 2.0,
12645                                    units: NumericSuffix::Mm,
12646                                },
12647                                y: Number {
12648                                    value: 3.0,
12649                                    units: NumericSuffix::Mm,
12650                                },
12651                            },
12652                        },
12653                        FixedPoint {
12654                            point: point1_id,
12655                            position: Point2d {
12656                                x: Number {
12657                                    value: 4.0,
12658                                    units: NumericSuffix::Mm,
12659                                },
12660                                y: Number {
12661                                    value: 5.0,
12662                                    units: NumericSuffix::Mm,
12663                                },
12664                            },
12665                        },
12666                    ],
12667                }),
12668            )
12669            .await
12670            .unwrap();
12671        insta::assert_snapshot!("test_add_fixed_multiple_points", src_delta.text.as_str());
12672        assert_eq!(
12673            scene_delta.new_graph.objects.len(),
12674            6,
12675            "{:#?}",
12676            scene_delta.new_graph.objects
12677        );
12678
12679        ctx.close().await;
12680        mock_ctx.close().await;
12681    }
12682
12683    #[tokio::test(flavor = "multi_thread")]
12684    async fn test_add_fixed_owned_point() {
12685        let initial_source = "\
12686sketch(on = XY) {
12687  line(start = [var 1, var 2], end = [var 3, var 4])
12688}
12689";
12690
12691        let program = Program::parse(initial_source).unwrap().0.unwrap();
12692
12693        let mut frontend = FrontendState::new();
12694
12695        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12696        let mock_ctx = ExecutorContext::new_mock(None).await;
12697        let version = Version(0);
12698
12699        frontend.hack_set_program(&ctx, program).await.unwrap();
12700        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12701        let sketch_id = sketch_object.id;
12702        let sketch = expect_sketch(sketch_object);
12703        let line_start_id = *sketch.segments.first().unwrap();
12704
12705        let (src_delta, scene_delta) = frontend
12706            .add_constraint(
12707                &mock_ctx,
12708                version,
12709                sketch_id,
12710                Constraint::Fixed(Fixed {
12711                    points: vec![FixedPoint {
12712                        point: line_start_id,
12713                        position: Point2d {
12714                            x: Number {
12715                                value: 2.0,
12716                                units: NumericSuffix::Mm,
12717                            },
12718                            y: Number {
12719                                value: 3.0,
12720                                units: NumericSuffix::Mm,
12721                            },
12722                        },
12723                    }],
12724                }),
12725            )
12726            .await
12727            .unwrap();
12728        insta::assert_snapshot!("test_add_fixed_owned_point", src_delta.text.as_str());
12729        assert_eq!(
12730            scene_delta.new_graph.objects.len(),
12731            6,
12732            "{:#?}",
12733            scene_delta.new_graph.objects
12734        );
12735
12736        ctx.close().await;
12737        mock_ctx.close().await;
12738    }
12739
12740    #[tokio::test(flavor = "multi_thread")]
12741    async fn test_radius_error_cases() {
12742        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12743        let mock_ctx = ExecutorContext::new_mock(None).await;
12744        let version = Version(0);
12745
12746        // Test: Single point should error
12747        let initial_source_point = "\
12748sketch(on = XY) {
12749  point(at = [var 1, var 2])
12750}
12751";
12752        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12753        let mut frontend_point = FrontendState::new();
12754        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12755        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12756        let sketch_id_point = sketch_object_point.id;
12757        let sketch_point = expect_sketch(sketch_object_point);
12758        let point_id = *sketch_point.segments.first().unwrap();
12759
12760        let constraint_point = Constraint::Radius(Radius {
12761            arc: point_id,
12762            radius: Number {
12763                value: 5.0,
12764                units: NumericSuffix::Mm,
12765            },
12766            label_position: None,
12767            source: Default::default(),
12768        });
12769        let result_point = frontend_point
12770            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12771            .await;
12772        assert!(result_point.is_err(), "Single point should error for radius");
12773
12774        // Test: Single line segment should error (only arc segments supported)
12775        let initial_source_line = "\
12776sketch(on = XY) {
12777  line(start = [var 1, var 2], end = [var 3, var 4])
12778}
12779";
12780        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12781        let mut frontend_line = FrontendState::new();
12782        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12783        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12784        let sketch_id_line = sketch_object_line.id;
12785        let sketch_line = expect_sketch(sketch_object_line);
12786        let line_id = *sketch_line.segments.first().unwrap();
12787
12788        let constraint_line = Constraint::Radius(Radius {
12789            arc: line_id,
12790            radius: Number {
12791                value: 5.0,
12792                units: NumericSuffix::Mm,
12793            },
12794            label_position: None,
12795            source: Default::default(),
12796        });
12797        let result_line = frontend_line
12798            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12799            .await;
12800        assert!(result_line.is_err(), "Single line segment should error for radius");
12801
12802        ctx.close().await;
12803        mock_ctx.close().await;
12804    }
12805
12806    #[tokio::test(flavor = "multi_thread")]
12807    async fn test_diameter_single_arc_segment() {
12808        let initial_source = "\
12809sketch(on = XY) {
12810  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12811}
12812";
12813
12814        let program = Program::parse(initial_source).unwrap().0.unwrap();
12815
12816        let mut frontend = FrontendState::new();
12817
12818        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12819        let mock_ctx = ExecutorContext::new_mock(None).await;
12820        let version = Version(0);
12821
12822        frontend.hack_set_program(&ctx, program).await.unwrap();
12823        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12824        let sketch_id = sketch_object.id;
12825        let sketch = expect_sketch(sketch_object);
12826        // Find the arc segment (not the points)
12827        let arc_id = sketch
12828            .segments
12829            .iter()
12830            .find(|&seg_id| {
12831                let obj = frontend.scene_graph.objects.get(seg_id.0);
12832                matches!(
12833                    obj.map(|o| &o.kind),
12834                    Some(ObjectKind::Segment {
12835                        segment: Segment::Arc(_)
12836                    })
12837                )
12838            })
12839            .unwrap();
12840
12841        let constraint = Constraint::Diameter(Diameter {
12842            arc: *arc_id,
12843            diameter: Number {
12844                value: 10.0,
12845                units: NumericSuffix::Mm,
12846            },
12847            label_position: None,
12848            source: Default::default(),
12849        });
12850        let (src_delta, scene_delta) = frontend
12851            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12852            .await
12853            .unwrap();
12854        insta::assert_snapshot!("test_diameter_single_arc_segment", src_delta.text.as_str());
12855        assert_eq!(
12856            scene_delta.new_graph.objects.len(),
12857            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
12858            "{:#?}",
12859            scene_delta.new_graph.objects
12860        );
12861
12862        ctx.close().await;
12863        mock_ctx.close().await;
12864    }
12865
12866    #[tokio::test(flavor = "multi_thread")]
12867    async fn test_diameter_single_arc_segment_with_label_position() {
12868        let initial_source = "\
12869sketch(on = XY) {
12870  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12871}
12872";
12873
12874        let program = Program::parse(initial_source).unwrap().0.unwrap();
12875        let mut frontend = FrontendState::new();
12876        let mock_ctx = ExecutorContext::new_mock(None).await;
12877        let version = Version(0);
12878
12879        frontend.program = program.clone();
12880        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12881        frontend.update_state_after_exec(outcome, true);
12882        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12883        let sketch_id = sketch_object.id;
12884        let sketch = expect_sketch(sketch_object);
12885        let arc_id = sketch
12886            .segments
12887            .iter()
12888            .find(|&seg_id| {
12889                let obj = frontend.scene_graph.objects.get(seg_id.0);
12890                matches!(
12891                    obj.map(|o| &o.kind),
12892                    Some(ObjectKind::Segment {
12893                        segment: Segment::Arc(_)
12894                    })
12895                )
12896            })
12897            .unwrap();
12898
12899        let label_position = Point2d {
12900            x: Number {
12901                value: 10.0,
12902                units: NumericSuffix::Mm,
12903            },
12904            y: Number {
12905                value: 11.0,
12906                units: NumericSuffix::Mm,
12907            },
12908        };
12909        let constraint = Constraint::Diameter(Diameter {
12910            arc: *arc_id,
12911            diameter: Number {
12912                value: 10.0,
12913                units: NumericSuffix::Mm,
12914            },
12915            label_position: Some(label_position.clone()),
12916            source: Default::default(),
12917        });
12918        let (src_delta, scene_delta) = frontend
12919            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12920            .await
12921            .unwrap();
12922        insta::assert_snapshot!(
12923            "test_diameter_single_arc_segment_with_label_position",
12924            src_delta.text.as_str()
12925        );
12926
12927        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12928        let sketch = expect_sketch(sketch_object);
12929        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12930        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12931            panic!("Expected constraint object");
12932        };
12933        let Constraint::Diameter(diameter) = constraint else {
12934            panic!("Expected diameter constraint");
12935        };
12936        assert_eq!(diameter.label_position, Some(label_position));
12937
12938        mock_ctx.close().await;
12939    }
12940
12941    #[tokio::test(flavor = "multi_thread")]
12942    async fn test_edit_diameter_constraint_label_position() {
12943        let initial_source = "\
12944sketch(on = XY) {
12945  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12946  diameter(arc1) == 10mm
12947}
12948";
12949
12950        let program = Program::parse(initial_source).unwrap().0.unwrap();
12951        let mut frontend = FrontendState::new();
12952        let mock_ctx = ExecutorContext::new_mock(None).await;
12953        let version = Version(0);
12954
12955        frontend.program = program.clone();
12956        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12957        frontend.update_state_after_exec(outcome, true);
12958        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12959        let sketch_id = sketch_object.id;
12960        let sketch = expect_sketch(sketch_object);
12961        let constraint_id = sketch.constraints[0];
12962        let label_position = Point2d {
12963            x: Number {
12964                value: 10.0,
12965                units: NumericSuffix::Mm,
12966            },
12967            y: Number {
12968                value: 11.0,
12969                units: NumericSuffix::Mm,
12970            },
12971        };
12972
12973        let (src_delta, scene_delta) = frontend
12974            .edit_distance_constraint_label_position(
12975                &mock_ctx,
12976                version,
12977                sketch_id,
12978                constraint_id,
12979                label_position.clone(),
12980                vec![],
12981            )
12982            .await
12983            .unwrap();
12984        insta::assert_snapshot!("test_edit_diameter_constraint_label_position", src_delta.text.as_str());
12985
12986        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12987        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12988            panic!("Expected constraint object");
12989        };
12990        let Constraint::Diameter(diameter) = constraint else {
12991            panic!("Expected diameter constraint");
12992        };
12993        assert_eq!(diameter.label_position, Some(label_position));
12994
12995        mock_ctx.close().await;
12996    }
12997
12998    #[tokio::test(flavor = "multi_thread")]
12999    async fn test_diameter_error_cases() {
13000        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13001        let mock_ctx = ExecutorContext::new_mock(None).await;
13002        let version = Version(0);
13003
13004        // Test: Single point should error
13005        let initial_source_point = "\
13006sketch(on = XY) {
13007  point(at = [var 1, var 2])
13008}
13009";
13010        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
13011        let mut frontend_point = FrontendState::new();
13012        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
13013        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
13014        let sketch_id_point = sketch_object_point.id;
13015        let sketch_point = expect_sketch(sketch_object_point);
13016        let point_id = *sketch_point.segments.first().unwrap();
13017
13018        let constraint_point = Constraint::Diameter(Diameter {
13019            arc: point_id,
13020            diameter: Number {
13021                value: 10.0,
13022                units: NumericSuffix::Mm,
13023            },
13024            label_position: None,
13025            source: Default::default(),
13026        });
13027        let result_point = frontend_point
13028            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
13029            .await;
13030        assert!(result_point.is_err(), "Single point should error for diameter");
13031
13032        // Test: Single line segment should error (only arc segments supported)
13033        let initial_source_line = "\
13034sketch(on = XY) {
13035  line(start = [var 1, var 2], end = [var 3, var 4])
13036}
13037";
13038        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
13039        let mut frontend_line = FrontendState::new();
13040        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
13041        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
13042        let sketch_id_line = sketch_object_line.id;
13043        let sketch_line = expect_sketch(sketch_object_line);
13044        let line_id = *sketch_line.segments.first().unwrap();
13045
13046        let constraint_line = Constraint::Diameter(Diameter {
13047            arc: line_id,
13048            diameter: Number {
13049                value: 10.0,
13050                units: NumericSuffix::Mm,
13051            },
13052            label_position: None,
13053            source: Default::default(),
13054        });
13055        let result_line = frontend_line
13056            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
13057            .await;
13058        assert!(result_line.is_err(), "Single line segment should error for diameter");
13059
13060        ctx.close().await;
13061        mock_ctx.close().await;
13062    }
13063
13064    #[tokio::test(flavor = "multi_thread")]
13065    async fn test_line_horizontal() {
13066        let initial_source = "\
13067sketch(on = XY) {
13068  line(start = [var 1, var 2], end = [var 3, var 4])
13069}
13070";
13071
13072        let program = Program::parse(initial_source).unwrap().0.unwrap();
13073
13074        let mut frontend = FrontendState::new();
13075
13076        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13077        let mock_ctx = ExecutorContext::new_mock(None).await;
13078        let version = Version(0);
13079
13080        frontend.hack_set_program(&ctx, program).await.unwrap();
13081        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13082        let sketch_id = sketch_object.id;
13083        let sketch = expect_sketch(sketch_object);
13084        let line1_id = *sketch.segments.get(2).unwrap();
13085
13086        let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
13087        let (src_delta, scene_delta) = frontend
13088            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13089            .await
13090            .unwrap();
13091        insta::assert_snapshot!("test_line_horizontal", src_delta.text.as_str());
13092        assert_eq!(
13093            scene_delta.new_graph.objects.len(),
13094            6,
13095            "{:#?}",
13096            scene_delta.new_graph.objects
13097        );
13098
13099        ctx.close().await;
13100        mock_ctx.close().await;
13101    }
13102
13103    #[tokio::test(flavor = "multi_thread")]
13104    async fn test_control_point_spline_edge_horizontal() {
13105        let initial_source = "\
13106@settings(experimentalFeatures = allow)
13107splineSketch = sketch(on = XY) {
13108  controlPointSpline1 = controlPointSpline(points = [
13109    [var 0mm, var 0mm],
13110    [var 10mm, var 20mm],
13111    [var 20mm, var 0mm],
13112  ])
13113}
13114";
13115
13116        let program = Program::parse(initial_source).unwrap().0.unwrap();
13117
13118        let mut frontend = FrontendState::new();
13119
13120        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13121        let mock_ctx = ExecutorContext::new_mock(None).await;
13122        let version = Version(0);
13123
13124        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13125        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13126        let sketch_id = sketch_object.id;
13127        let sketch = expect_sketch(sketch_object);
13128        let spline_id = sketch
13129            .segments
13130            .iter()
13131            .copied()
13132            .find(|seg_id| {
13133                matches!(
13134                    &frontend.scene_graph.objects[seg_id.0].kind,
13135                    ObjectKind::Segment {
13136                        segment: Segment::ControlPointSpline(_)
13137                    }
13138                )
13139            })
13140            .expect("Expected a control point spline segment in sketch");
13141        let edge_id = frontend
13142            .scene_graph
13143            .objects
13144            .iter()
13145            .find_map(|obj| match &obj.kind {
13146                ObjectKind::Segment {
13147                    segment: Segment::Line(line),
13148                } if line.owner == Some(spline_id) => Some(obj.id),
13149                _ => None,
13150            })
13151            .expect("Expected an owned control-polygon edge");
13152
13153        let constraint = Constraint::Horizontal(Horizontal::Line { line: edge_id });
13154        let (src_delta, _) = frontend
13155            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13156            .await
13157            .unwrap();
13158        assert!(
13159            src_delta.text.contains("horizontal(controlPointSpline1.edges[0])"),
13160            "Expected horizontal constraint on spline edge, got: {}",
13161            src_delta.text
13162        );
13163
13164        ctx.close().await;
13165        mock_ctx.close().await;
13166    }
13167
13168    #[tokio::test(flavor = "multi_thread")]
13169    async fn test_control_point_spline_edge_angle() {
13170        let initial_source = "\
13171@settings(experimentalFeatures = allow)
13172splineSketch = sketch(on = XY) {
13173  controlPointSpline1 = controlPointSpline(points = [
13174    [var 0mm, var 0mm],
13175    [var 10mm, var 20mm],
13176    [var 20mm, var 0mm],
13177  ])
13178
13179  line1 = line(start = [var 40mm, var 0mm], end = [var 60mm, var 10mm])
13180}
13181";
13182
13183        let program = Program::parse(initial_source).unwrap().0.unwrap();
13184
13185        let mut frontend = FrontendState::new();
13186
13187        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13188        let mock_ctx = ExecutorContext::new_mock(None).await;
13189        let version = Version(0);
13190
13191        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13192        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13193        let sketch_id = sketch_object.id;
13194        let sketch = expect_sketch(sketch_object);
13195        let spline_id = sketch
13196            .segments
13197            .iter()
13198            .copied()
13199            .find(|seg_id| {
13200                matches!(
13201                    &frontend.scene_graph.objects[seg_id.0].kind,
13202                    ObjectKind::Segment {
13203                        segment: Segment::ControlPointSpline(_)
13204                    }
13205                )
13206            })
13207            .expect("Expected a control point spline segment in sketch");
13208        let edge_id = frontend
13209            .scene_graph
13210            .objects
13211            .iter()
13212            .find_map(|obj| match &obj.kind {
13213                ObjectKind::Segment {
13214                    segment: Segment::Line(line),
13215                } if line.owner == Some(spline_id) => Some(obj.id),
13216                _ => None,
13217            })
13218            .expect("Expected an owned control-polygon edge");
13219        let line1_id = frontend
13220            .scene_graph
13221            .objects
13222            .iter()
13223            .find_map(|obj| match &obj.kind {
13224                ObjectKind::Segment {
13225                    segment: Segment::Line(line),
13226                } if line.owner.is_none() && obj.label == "line1" => Some(obj.id),
13227                _ => None,
13228            })
13229            .or_else(|| {
13230                sketch.segments.iter().copied().find(|seg_id| {
13231                    matches!(
13232                        &frontend.scene_graph.objects[seg_id.0].kind,
13233                        ObjectKind::Segment {
13234                            segment: Segment::Line(line),
13235                        } if line.owner.is_none()
13236                    )
13237                })
13238            })
13239            .expect("Expected a standalone line segment in sketch");
13240
13241        let constraint = Constraint::Angle(Angle {
13242            lines: vec![line1_id, edge_id],
13243            angle: Number {
13244                value: 30.0,
13245                units: NumericSuffix::Deg,
13246            },
13247            sector: None,
13248            inverse: None,
13249            label_position: None,
13250            source: Default::default(),
13251        });
13252        let (src_delta, _) = frontend
13253            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13254            .await
13255            .unwrap();
13256        assert!(
13257            src_delta
13258                .text
13259                .contains("angle([line1, controlPointSpline1.edges[0]]) == 30deg"),
13260            "Expected angle constraint on spline edge, got: {}",
13261            src_delta.text
13262        );
13263
13264        ctx.close().await;
13265        mock_ctx.close().await;
13266    }
13267
13268    #[tokio::test(flavor = "multi_thread")]
13269    async fn test_ui_scene_graph_hides_same_spline_coincident_constraints() {
13270        let initial_source = "\
13271@settings(experimentalFeatures = allow)
13272splineSketch = sketch(on = XY) {
13273  spline1 = controlPointSpline(points = [
13274    [var 0mm, var 0mm],
13275    [var 10mm, var 20mm],
13276    [var 20mm, var 0mm],
13277  ])
13278  line1 = line(start = [var 0mm, var 0mm], end = [var -10mm, var 0mm])
13279  coincident([spline1.controls[1], spline1.edges[0]])
13280  coincident([spline1.controls[0], line1])
13281}
13282";
13283
13284        let program = Program::parse(initial_source).unwrap().0.unwrap();
13285
13286        let mut frontend = FrontendState::new();
13287
13288        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13289        let mock_ctx = ExecutorContext::new_mock(None).await;
13290
13291        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13292
13293        let ui_scene_graph = frontend.scene_graph_for_ui();
13294        let sketch_object = find_first_sketch_object(&ui_scene_graph).unwrap();
13295        let sketch = expect_sketch(sketch_object);
13296
13297        assert_eq!(
13298            sketch.constraints.len(),
13299            1,
13300            "Expected only the external coincident constraint to remain visible in the UI scene graph"
13301        );
13302
13303        let visible_constraints = ui_scene_graph
13304            .objects
13305            .iter()
13306            .filter_map(|object| match &object.kind {
13307                ObjectKind::Constraint {
13308                    constraint: Constraint::Coincident(coincident),
13309                } => Some(coincident.clone()),
13310                _ => None,
13311            })
13312            .collect::<Vec<_>>();
13313
13314        assert_eq!(
13315            visible_constraints.len(),
13316            1,
13317            "Expected only one coincident constraint object in the UI scene graph"
13318        );
13319        assert_eq!(
13320            visible_constraints[0].get_segments().len(),
13321            2,
13322            "Expected the remaining visible coincident constraint to reference two segments"
13323        );
13324
13325        ctx.close().await;
13326        mock_ctx.close().await;
13327    }
13328
13329    #[tokio::test(flavor = "multi_thread")]
13330    async fn test_edit_control_point_spline_can_append_control_point() {
13331        let initial_source = "\
13332@settings(experimentalFeatures = allow)
13333splineSketch = sketch(on = XY) {
13334  controlPointSpline(points = [
13335    [var 0mm, var 0mm],
13336    [var 10mm, var 20mm],
13337    [var 20mm, var 0mm],
13338  ])
13339}
13340";
13341
13342        let program = Program::parse(initial_source).unwrap().0.unwrap();
13343
13344        let mut frontend = FrontendState::new();
13345
13346        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13347        let mock_ctx = ExecutorContext::new_mock(None).await;
13348        let version = Version(0);
13349
13350        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13351        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13352        let sketch_id = sketch_object.id;
13353        let sketch = expect_sketch(sketch_object);
13354        let spline_id = sketch
13355            .segments
13356            .iter()
13357            .copied()
13358            .find(|seg_id| {
13359                matches!(
13360                    &frontend.scene_graph.objects[seg_id.0].kind,
13361                    ObjectKind::Segment {
13362                        segment: Segment::ControlPointSpline(_)
13363                    }
13364                )
13365            })
13366            .expect("Expected a control point spline segment in sketch");
13367
13368        let ctor = ControlPointSplineCtor {
13369            points: vec![
13370                Point2d {
13371                    x: Expr::Var(Number {
13372                        value: 0.0,
13373                        units: NumericSuffix::Mm,
13374                    }),
13375                    y: Expr::Var(Number {
13376                        value: 0.0,
13377                        units: NumericSuffix::Mm,
13378                    }),
13379                },
13380                Point2d {
13381                    x: Expr::Var(Number {
13382                        value: 10.0,
13383                        units: NumericSuffix::Mm,
13384                    }),
13385                    y: Expr::Var(Number {
13386                        value: 20.0,
13387                        units: NumericSuffix::Mm,
13388                    }),
13389                },
13390                Point2d {
13391                    x: Expr::Var(Number {
13392                        value: 20.0,
13393                        units: NumericSuffix::Mm,
13394                    }),
13395                    y: Expr::Var(Number {
13396                        value: 0.0,
13397                        units: NumericSuffix::Mm,
13398                    }),
13399                },
13400                Point2d {
13401                    x: Expr::Var(Number {
13402                        value: 30.0,
13403                        units: NumericSuffix::Mm,
13404                    }),
13405                    y: Expr::Var(Number {
13406                        value: 10.0,
13407                        units: NumericSuffix::Mm,
13408                    }),
13409                },
13410            ],
13411            construction: None,
13412        };
13413
13414        let segments = vec![ExistingSegmentCtor {
13415            id: spline_id,
13416            ctor: SegmentCtor::ControlPointSpline(ctor),
13417        }];
13418        let (src_delta, scene_delta) = frontend
13419            .edit_segments(&mock_ctx, version, sketch_id, segments)
13420            .await
13421            .unwrap();
13422
13423        assert!(
13424            src_delta.text.contains("[var 30mm, var 10mm]"),
13425            "Expected appended spline control point in source, got: {}",
13426            src_delta.text
13427        );
13428
13429        assert!(
13430            scene_delta.invalidates_ids,
13431            "Expected appending a spline control point to invalidate ids"
13432        );
13433        let updated_spline = scene_delta
13434            .new_graph
13435            .objects
13436            .iter()
13437            .find_map(|obj| match &obj.kind {
13438                ObjectKind::Segment {
13439                    segment: Segment::ControlPointSpline(updated_spline),
13440                } if updated_spline.controls.len() == 4 => Some(updated_spline),
13441                _ => None,
13442            })
13443            .expect("Expected edited scene graph to contain a four-point control point spline");
13444        assert_eq!(
13445            updated_spline.controls.len(),
13446            4,
13447            "Expected edited spline to expose four control points"
13448        );
13449
13450        ctx.close().await;
13451        mock_ctx.close().await;
13452    }
13453
13454    #[tokio::test(flavor = "multi_thread")]
13455    async fn test_line_vertical() {
13456        let initial_source = "\
13457sketch(on = XY) {
13458  line(start = [var 1, var 2], end = [var 3, var 4])
13459}
13460";
13461
13462        let program = Program::parse(initial_source).unwrap().0.unwrap();
13463
13464        let mut frontend = FrontendState::new();
13465
13466        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13467        let mock_ctx = ExecutorContext::new_mock(None).await;
13468        let version = Version(0);
13469
13470        frontend.hack_set_program(&ctx, program).await.unwrap();
13471        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13472        let sketch_id = sketch_object.id;
13473        let sketch = expect_sketch(sketch_object);
13474        let line1_id = *sketch.segments.get(2).unwrap();
13475
13476        let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
13477        let (src_delta, scene_delta) = frontend
13478            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13479            .await
13480            .unwrap();
13481        insta::assert_snapshot!("test_line_vertical", src_delta.text.as_str());
13482        assert_eq!(
13483            scene_delta.new_graph.objects.len(),
13484            6,
13485            "{:#?}",
13486            scene_delta.new_graph.objects
13487        );
13488
13489        ctx.close().await;
13490        mock_ctx.close().await;
13491    }
13492
13493    #[tokio::test(flavor = "multi_thread")]
13494    async fn test_points_vertical() {
13495        let initial_source = "\
13496sketch001 = sketch(on = XY) {
13497  p0 = point(at = [var -2.23mm, var 3.1mm])
13498  pf = point(at = [4, 4])
13499}
13500";
13501
13502        let program = Program::parse(initial_source).unwrap().0.unwrap();
13503
13504        let mut frontend = FrontendState::new();
13505
13506        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13507        let mock_ctx = ExecutorContext::new_mock(None).await;
13508        let version = Version(0);
13509
13510        frontend.hack_set_program(&ctx, program).await.unwrap();
13511        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13512        let sketch_id = sketch_object.id;
13513        let sketch = expect_sketch(sketch_object);
13514        let point_ids = vec![
13515            sketch.segments.first().unwrap().to_owned(),
13516            sketch.segments.get(1).unwrap().to_owned(),
13517        ];
13518
13519        let constraint = Constraint::Vertical(Vertical::Points {
13520            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
13521        });
13522        let (src_delta, scene_delta) = frontend
13523            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13524            .await
13525            .unwrap();
13526        insta::assert_snapshot!("test_points_vertical", src_delta.text.as_str());
13527        assert_eq!(
13528            scene_delta.new_graph.objects.len(),
13529            5,
13530            "{:#?}",
13531            scene_delta.new_graph.objects
13532        );
13533
13534        ctx.close().await;
13535        mock_ctx.close().await;
13536    }
13537
13538    #[tokio::test(flavor = "multi_thread")]
13539    async fn test_points_horizontal() {
13540        let initial_source = "\
13541sketch001 = sketch(on = XY) {
13542  p0 = point(at = [var -2.23mm, var 3.1mm])
13543  pf = point(at = [4, 4])
13544}
13545";
13546
13547        let program = Program::parse(initial_source).unwrap().0.unwrap();
13548
13549        let mut frontend = FrontendState::new();
13550
13551        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13552        let mock_ctx = ExecutorContext::new_mock(None).await;
13553        let version = Version(0);
13554
13555        frontend.hack_set_program(&ctx, program).await.unwrap();
13556        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13557        let sketch_id = sketch_object.id;
13558        let sketch = expect_sketch(sketch_object);
13559        let point_ids = vec![
13560            sketch.segments.first().unwrap().to_owned(),
13561            sketch.segments.get(1).unwrap().to_owned(),
13562        ];
13563
13564        let constraint = Constraint::Horizontal(Horizontal::Points {
13565            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
13566        });
13567        let (src_delta, scene_delta) = frontend
13568            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13569            .await
13570            .unwrap();
13571        insta::assert_snapshot!("test_points_horizontal", src_delta.text.as_str());
13572        assert_eq!(
13573            scene_delta.new_graph.objects.len(),
13574            5,
13575            "{:#?}",
13576            scene_delta.new_graph.objects
13577        );
13578
13579        ctx.close().await;
13580        mock_ctx.close().await;
13581    }
13582
13583    #[tokio::test(flavor = "multi_thread")]
13584    async fn test_point_horizontal_with_origin() {
13585        let initial_source = "\
13586sketch001 = sketch(on = XY) {
13587  p0 = point(at = [var -2.23mm, var 3.1mm])
13588}
13589";
13590
13591        let program = Program::parse(initial_source).unwrap().0.unwrap();
13592
13593        let mut frontend = FrontendState::new();
13594
13595        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13596        let mock_ctx = ExecutorContext::new_mock(None).await;
13597        let version = Version(0);
13598
13599        frontend.hack_set_program(&ctx, program).await.unwrap();
13600        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13601        let sketch_id = sketch_object.id;
13602        let sketch = expect_sketch(sketch_object);
13603        let point_id = *sketch.segments.first().unwrap();
13604
13605        let constraint = Constraint::Horizontal(Horizontal::Points {
13606            points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
13607        });
13608        let (src_delta, scene_delta) = frontend
13609            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13610            .await
13611            .unwrap();
13612        insta::assert_snapshot!("test_point_horizontal_with_origin", src_delta.text.as_str());
13613        assert_eq!(
13614            scene_delta.new_graph.objects.len(),
13615            4,
13616            "{:#?}",
13617            scene_delta.new_graph.objects
13618        );
13619
13620        ctx.close().await;
13621        mock_ctx.close().await;
13622    }
13623
13624    #[tokio::test(flavor = "multi_thread")]
13625    async fn test_lines_equal_length() {
13626        let initial_source = "\
13627sketch(on = XY) {
13628  line(start = [var 1, var 2], end = [var 3, var 4])
13629  line(start = [var 5, var 6], end = [var 7, var 8])
13630}
13631";
13632
13633        let program = Program::parse(initial_source).unwrap().0.unwrap();
13634
13635        let mut frontend = FrontendState::new();
13636
13637        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13638        let mock_ctx = ExecutorContext::new_mock(None).await;
13639        let version = Version(0);
13640
13641        frontend.hack_set_program(&ctx, program).await.unwrap();
13642        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13643        let sketch_id = sketch_object.id;
13644        let sketch = expect_sketch(sketch_object);
13645        let line1_id = *sketch.segments.get(2).unwrap();
13646        let line2_id = *sketch.segments.get(5).unwrap();
13647
13648        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13649            lines: vec![line1_id, line2_id],
13650        });
13651        let (src_delta, scene_delta) = frontend
13652            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13653            .await
13654            .unwrap();
13655        insta::assert_snapshot!("test_lines_equal_length", src_delta.text.as_str());
13656        assert_eq!(
13657            scene_delta.new_graph.objects.len(),
13658            9,
13659            "{:#?}",
13660            scene_delta.new_graph.objects
13661        );
13662
13663        ctx.close().await;
13664        mock_ctx.close().await;
13665    }
13666
13667    #[tokio::test(flavor = "multi_thread")]
13668    async fn test_add_constraint_multi_line_equal_length() {
13669        let initial_source = "\
13670sketch(on = XY) {
13671  line(start = [var 1, var 2], end = [var 3, var 4])
13672  line(start = [var 5, var 6], end = [var 7, var 8])
13673  line(start = [var 9, var 10], end = [var 11, var 12])
13674}
13675";
13676
13677        let program = Program::parse(initial_source).unwrap().0.unwrap();
13678
13679        let mut frontend = FrontendState::new();
13680        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13681        let mock_ctx = ExecutorContext::new_mock(None).await;
13682        let version = Version(0);
13683
13684        frontend.hack_set_program(&ctx, program).await.unwrap();
13685        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13686        let sketch_id = sketch_object.id;
13687        let sketch = expect_sketch(sketch_object);
13688        let line1_id = *sketch.segments.get(2).unwrap();
13689        let line2_id = *sketch.segments.get(5).unwrap();
13690        let line3_id = *sketch.segments.get(8).unwrap();
13691
13692        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13693            lines: vec![line1_id, line2_id, line3_id],
13694        });
13695        let (src_delta, scene_delta) = frontend
13696            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13697            .await
13698            .unwrap();
13699        insta::assert_snapshot!("test_add_constraint_multi_line_equal_length", src_delta.text.as_str());
13700        let constraints = scene_delta
13701            .new_graph
13702            .objects
13703            .iter()
13704            .filter_map(|obj| {
13705                let ObjectKind::Constraint { constraint } = &obj.kind else {
13706                    return None;
13707                };
13708                Some(constraint)
13709            })
13710            .collect::<Vec<_>>();
13711
13712        assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
13713        let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
13714            panic!("expected equal length constraint, got {:?}", constraints[0]);
13715        };
13716        assert_eq!(lines_equal_length.lines.len(), 3);
13717
13718        ctx.close().await;
13719        mock_ctx.close().await;
13720    }
13721
13722    #[tokio::test(flavor = "multi_thread")]
13723    async fn test_lines_parallel() {
13724        let initial_source = "\
13725sketch(on = XY) {
13726  line(start = [var 1, var 2], end = [var 3, var 4])
13727  line(start = [var 5, var 6], end = [var 7, var 8])
13728}
13729";
13730
13731        let program = Program::parse(initial_source).unwrap().0.unwrap();
13732
13733        let mut frontend = FrontendState::new();
13734
13735        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13736        let mock_ctx = ExecutorContext::new_mock(None).await;
13737        let version = Version(0);
13738
13739        frontend.hack_set_program(&ctx, program).await.unwrap();
13740        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13741        let sketch_id = sketch_object.id;
13742        let sketch = expect_sketch(sketch_object);
13743        let line1_id = *sketch.segments.get(2).unwrap();
13744        let line2_id = *sketch.segments.get(5).unwrap();
13745
13746        let constraint = Constraint::Parallel(Parallel {
13747            lines: vec![line1_id, line2_id],
13748        });
13749        let (src_delta, scene_delta) = frontend
13750            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13751            .await
13752            .unwrap();
13753        insta::assert_snapshot!("test_lines_parallel", src_delta.text.as_str());
13754        assert_eq!(
13755            scene_delta.new_graph.objects.len(),
13756            9,
13757            "{:#?}",
13758            scene_delta.new_graph.objects
13759        );
13760
13761        ctx.close().await;
13762        mock_ctx.close().await;
13763    }
13764
13765    #[tokio::test(flavor = "multi_thread")]
13766    async fn test_lines_parallel_multiline() {
13767        let initial_source = "\
13768sketch(on = XY) {
13769  line(start = [var 1, var 2], end = [var 3, var 4])
13770  line(start = [var 5, var 6], end = [var 7, var 8])
13771  line(start = [var 9, var 10], end = [var 11, var 12])
13772}
13773";
13774
13775        let program = Program::parse(initial_source).unwrap().0.unwrap();
13776
13777        let mut frontend = FrontendState::new();
13778
13779        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13780        let mock_ctx = ExecutorContext::new_mock(None).await;
13781        let version = Version(0);
13782
13783        frontend.hack_set_program(&ctx, program).await.unwrap();
13784        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13785        let sketch_id = sketch_object.id;
13786        let sketch = expect_sketch(sketch_object);
13787        let line1_id = *sketch.segments.get(2).unwrap();
13788        let line2_id = *sketch.segments.get(5).unwrap();
13789        let line3_id = *sketch.segments.get(8).unwrap();
13790
13791        let constraint = Constraint::Parallel(Parallel {
13792            lines: vec![line1_id, line2_id, line3_id],
13793        });
13794        let (src_delta, scene_delta) = frontend
13795            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13796            .await
13797            .unwrap();
13798        insta::assert_snapshot!("test_lines_parallel_multiline", src_delta.text.as_str());
13799
13800        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
13801        let sketch = expect_sketch(sketch_object);
13802        assert_eq!(sketch.constraints.len(), 1);
13803
13804        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
13805        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
13806            panic!("Expected constraint object");
13807        };
13808        let Constraint::Parallel(parallel) = constraint else {
13809            panic!("Expected parallel constraint");
13810        };
13811        assert_eq!(parallel.lines.len(), 3);
13812
13813        ctx.close().await;
13814        mock_ctx.close().await;
13815    }
13816
13817    #[tokio::test(flavor = "multi_thread")]
13818    async fn test_lines_perpendicular() {
13819        let initial_source = "\
13820sketch(on = XY) {
13821  line(start = [var 1, var 2], end = [var 3, var 4])
13822  line(start = [var 5, var 6], end = [var 7, var 8])
13823}
13824";
13825
13826        let program = Program::parse(initial_source).unwrap().0.unwrap();
13827
13828        let mut frontend = FrontendState::new();
13829
13830        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13831        let mock_ctx = ExecutorContext::new_mock(None).await;
13832        let version = Version(0);
13833
13834        frontend.hack_set_program(&ctx, program).await.unwrap();
13835        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13836        let sketch_id = sketch_object.id;
13837        let sketch = expect_sketch(sketch_object);
13838        let line1_id = *sketch.segments.get(2).unwrap();
13839        let line2_id = *sketch.segments.get(5).unwrap();
13840
13841        let constraint = Constraint::Perpendicular(Perpendicular {
13842            lines: vec![line1_id, line2_id],
13843        });
13844        let (src_delta, scene_delta) = frontend
13845            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13846            .await
13847            .unwrap();
13848        insta::assert_snapshot!("test_lines_perpendicular", src_delta.text.as_str());
13849        assert_eq!(
13850            scene_delta.new_graph.objects.len(),
13851            9,
13852            "{:#?}",
13853            scene_delta.new_graph.objects
13854        );
13855
13856        ctx.close().await;
13857        mock_ctx.close().await;
13858    }
13859
13860    #[tokio::test(flavor = "multi_thread")]
13861    async fn test_lines_angle() {
13862        let initial_source = "\
13863sketch(on = XY) {
13864  line(start = [var 1, var 2], end = [var 3, var 4])
13865  line(start = [var 5, var 6], end = [var 7, var 8])
13866}
13867";
13868
13869        let program = Program::parse(initial_source).unwrap().0.unwrap();
13870
13871        let mut frontend = FrontendState::new();
13872
13873        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13874        let mock_ctx = ExecutorContext::new_mock(None).await;
13875        let version = Version(0);
13876
13877        frontend.hack_set_program(&ctx, program).await.unwrap();
13878        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13879        let sketch_id = sketch_object.id;
13880        let sketch = expect_sketch(sketch_object);
13881        let line1_id = *sketch.segments.get(2).unwrap();
13882        let line2_id = *sketch.segments.get(5).unwrap();
13883
13884        let constraint = Constraint::Angle(Angle {
13885            lines: vec![line1_id, line2_id],
13886            angle: Number {
13887                value: 30.0,
13888                units: NumericSuffix::Deg,
13889            },
13890            sector: None,
13891            inverse: None,
13892            label_position: None,
13893            source: Default::default(),
13894        });
13895        let (src_delta, scene_delta) = frontend
13896            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13897            .await
13898            .unwrap();
13899        insta::assert_snapshot!("test_lines_angle", src_delta.text.as_str());
13900        assert_eq!(
13901            scene_delta.new_graph.objects.len(),
13902            9,
13903            "{:#?}",
13904            scene_delta.new_graph.objects
13905        );
13906
13907        ctx.close().await;
13908        mock_ctx.close().await;
13909    }
13910
13911    #[tokio::test(flavor = "multi_thread")]
13912    async fn test_lines_angle_with_sector_uses_angle_dimension() {
13913        let initial_source = "\
13914sketch(on = XY) {
13915  line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
13916  line(start = [var 0mm, var 0mm], end = [var 0mm, var 4mm])
13917}
13918";
13919
13920        let program = Program::parse(initial_source).unwrap().0.unwrap();
13921
13922        let mut frontend = FrontendState::new();
13923
13924        let mock_ctx = ExecutorContext::new_mock(None).await;
13925        let version = Version(0);
13926
13927        frontend.program = program.clone();
13928        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13929        frontend.update_state_after_exec(outcome, true);
13930        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13931        let sketch_id = sketch_object.id;
13932        let sketch = expect_sketch(sketch_object);
13933        let line1_id = *sketch.segments.get(2).unwrap();
13934        let line2_id = *sketch.segments.get(5).unwrap();
13935
13936        let constraint = Constraint::Angle(Angle {
13937            lines: vec![line1_id, line2_id],
13938            angle: Number {
13939                value: 270.0,
13940                units: NumericSuffix::Deg,
13941            },
13942            sector: Some(1),
13943            inverse: Some(true),
13944            label_position: Some(Point2d {
13945                x: Number {
13946                    value: -0.73,
13947                    units: NumericSuffix::Mm,
13948                },
13949                y: Number {
13950                    value: 0.75,
13951                    units: NumericSuffix::Mm,
13952                },
13953            }),
13954            source: Default::default(),
13955        });
13956        let (src_delta, _) = frontend
13957            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13958            .await
13959            .unwrap();
13960        assert_eq!(
13961            src_delta.text.as_str(),
13962            "\
13963sketch(on = XY) {
13964  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
13965  line2 = line(start = [var 0mm, var 0mm], end = [var 0mm, var 4mm])
13966  angleDimension(
13967  lines = [line1, line2],
13968  sector = 1,
13969  inverse = true,
13970  labelPosition = [-0.73mm, 0.75mm],
13971) == 270deg
13972}
13973"
13974        );
13975
13976        mock_ctx.close().await;
13977    }
13978
13979    #[tokio::test(flavor = "multi_thread")]
13980    async fn test_segments_tangent() {
13981        let initial_source = "\
13982sketch(on = XY) {
13983  line(start = [var 1, var 2], end = [var 3, var 4])
13984  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13985}
13986";
13987
13988        let program = Program::parse(initial_source).unwrap().0.unwrap();
13989
13990        let mut frontend = FrontendState::new();
13991
13992        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13993        let mock_ctx = ExecutorContext::new_mock(None).await;
13994        let version = Version(0);
13995
13996        frontend.hack_set_program(&ctx, program).await.unwrap();
13997        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13998        let sketch_id = sketch_object.id;
13999        let sketch = expect_sketch(sketch_object);
14000        let line1_id = *sketch.segments.get(2).unwrap();
14001        let arc1_id = *sketch.segments.get(6).unwrap();
14002
14003        let constraint = Constraint::Tangent(Tangent {
14004            input: vec![line1_id, arc1_id],
14005        });
14006        let (src_delta, scene_delta) = frontend
14007            .add_constraint(&mock_ctx, version, sketch_id, constraint)
14008            .await
14009            .unwrap();
14010        insta::assert_snapshot!("test_segments_tangent", src_delta.text.as_str());
14011        assert_eq!(
14012            scene_delta.new_graph.objects.len(),
14013            10,
14014            "{:#?}",
14015            scene_delta.new_graph.objects
14016        );
14017
14018        ctx.close().await;
14019        mock_ctx.close().await;
14020    }
14021
14022    #[tokio::test(flavor = "multi_thread")]
14023    async fn test_point_midpoint() {
14024        let initial_source = "\
14025sketch(on = XY) {
14026  point(at = [var 1, var 1])
14027  line(start = [var 0, var 0], end = [var 6, var 4])
14028}
14029";
14030
14031        let program = Program::parse(initial_source).unwrap().0.unwrap();
14032
14033        let mut frontend = FrontendState::new();
14034
14035        let ctx = ExecutorContext::new_mock(None).await;
14036        let version = Version(0);
14037
14038        frontend.program = program.clone();
14039        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14040        frontend.update_state_after_exec(outcome, true);
14041        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14042        let sketch_id = sketch_object.id;
14043        let sketch = expect_sketch(sketch_object);
14044        let point_id = *sketch.segments.first().unwrap();
14045        let line_id = *sketch.segments.get(3).unwrap();
14046
14047        let constraint = Constraint::Midpoint(Midpoint {
14048            point: ConstraintSegment::from(point_id),
14049            segment: line_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_point_midpoint", src_delta.text.as_str());
14056        assert_eq!(
14057            scene_delta.new_graph.objects.len(),
14058            7,
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_segments_symmetric() {
14068        let initial_source = "\
14069sketch(on = XY) {
14070  line(start = [var 0, var 0], end = [var 0, var 4])
14071  line(start = [var 4, var 0], end = [var 4, var 4])
14072  line(start = [var 2, var -1], end = [var 2, var 5])
14073}
14074";
14075
14076        let program = Program::parse(initial_source).unwrap().0.unwrap();
14077
14078        let mut frontend = FrontendState::new();
14079
14080        let ctx = ExecutorContext::new_mock(None).await;
14081        let version = Version(0);
14082
14083        frontend.program = program.clone();
14084        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14085        frontend.update_state_after_exec(outcome, true);
14086        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14087        let sketch_id = sketch_object.id;
14088        let sketch = expect_sketch(sketch_object);
14089        let line1_id = *sketch.segments.get(2).unwrap();
14090        let line2_id = *sketch.segments.get(5).unwrap();
14091        let axis_id = *sketch.segments.get(8).unwrap();
14092
14093        let constraint = Constraint::Symmetric(Symmetric {
14094            input: vec![line1_id, line2_id],
14095            axis: axis_id,
14096        });
14097        let (src_delta, scene_delta) = frontend
14098            .add_constraint(&ctx, version, sketch_id, constraint)
14099            .await
14100            .unwrap();
14101        insta::assert_snapshot!("test_segments_symmetric", src_delta.text.as_str());
14102        assert_eq!(
14103            scene_delta.new_graph.objects.len(),
14104            12,
14105            "{:#?}",
14106            scene_delta.new_graph.objects
14107        );
14108
14109        ctx.close().await;
14110    }
14111
14112    #[tokio::test(flavor = "multi_thread")]
14113    async fn test_point_arc_midpoint() {
14114        let initial_source = "\
14115sketch(on = XY) {
14116  point(at = [var 6, var 3])
14117  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
14118}
14119";
14120
14121        let program = Program::parse(initial_source).unwrap().0.unwrap();
14122
14123        let mut frontend = FrontendState::new();
14124
14125        let ctx = ExecutorContext::new_mock(None).await;
14126        let version = Version(0);
14127
14128        frontend.program = program.clone();
14129        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14130        frontend.update_state_after_exec(outcome, true);
14131        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14132        let sketch_id = sketch_object.id;
14133        let sketch = expect_sketch(sketch_object);
14134        let point_id = *sketch.segments.first().unwrap();
14135        let arc_id = *sketch.segments.get(4).unwrap();
14136
14137        let constraint = Constraint::Midpoint(Midpoint {
14138            point: ConstraintSegment::from(point_id),
14139            segment: arc_id,
14140        });
14141        let (src_delta, scene_delta) = frontend
14142            .add_constraint(&ctx, version, sketch_id, constraint)
14143            .await
14144            .unwrap();
14145        insta::assert_snapshot!("test_point_arc_midpoint", src_delta.text.as_str());
14146        assert_eq!(
14147            scene_delta.new_graph.objects.len(),
14148            8,
14149            "{:#?}",
14150            scene_delta.new_graph.objects
14151        );
14152
14153        ctx.close().await;
14154    }
14155
14156    #[tokio::test(flavor = "multi_thread")]
14157    async fn test_origin_line_midpoint() {
14158        let initial_source = "\
14159sketch(on = XY) {
14160  line(start = [var 0, var 0], end = [var 6, var 4])
14161}
14162";
14163
14164        let program = Program::parse(initial_source).unwrap().0.unwrap();
14165
14166        let mut frontend = FrontendState::new();
14167
14168        let ctx = ExecutorContext::new_mock(None).await;
14169        let version = Version(0);
14170
14171        frontend.program = program.clone();
14172        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14173        frontend.update_state_after_exec(outcome, true);
14174        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14175        let sketch_id = sketch_object.id;
14176        let sketch = expect_sketch(sketch_object);
14177        let line_id = *sketch.segments.get(2).unwrap();
14178
14179        let constraint = Constraint::Midpoint(Midpoint {
14180            point: ConstraintSegment::ORIGIN,
14181            segment: line_id,
14182        });
14183        let (src_delta, scene_delta) = frontend
14184            .add_constraint(&ctx, version, sketch_id, constraint)
14185            .await
14186            .unwrap();
14187        insta::assert_snapshot!("test_origin_line_midpoint", src_delta.text.as_str());
14188        assert_eq!(
14189            scene_delta.new_graph.objects.len(),
14190            6,
14191            "{:#?}",
14192            scene_delta.new_graph.objects
14193        );
14194
14195        ctx.close().await;
14196    }
14197
14198    #[tokio::test(flavor = "multi_thread")]
14199    async fn test_origin_arc_midpoint() {
14200        let initial_source = "\
14201sketch(on = XY) {
14202  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
14203}
14204";
14205
14206        let program = Program::parse(initial_source).unwrap().0.unwrap();
14207
14208        let mut frontend = FrontendState::new();
14209
14210        let ctx = ExecutorContext::new_mock(None).await;
14211        let version = Version(0);
14212
14213        frontend.program = program.clone();
14214        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14215        frontend.update_state_after_exec(outcome, true);
14216        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14217        let sketch_id = sketch_object.id;
14218        let sketch = expect_sketch(sketch_object);
14219        let arc_id = *sketch.segments.get(3).unwrap();
14220
14221        let constraint = Constraint::Midpoint(Midpoint {
14222            point: ConstraintSegment::ORIGIN,
14223            segment: arc_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_origin_arc_midpoint", src_delta.text.as_str());
14230        assert_eq!(
14231            scene_delta.new_graph.objects.len(),
14232            7,
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_segments_symmetric_arcs() {
14242        let initial_source = "\
14243sketch(on = XY) {
14244  arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
14245  arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
14246  line(start = [var 0, var -10], end = [var 0, var 10])
14247}
14248";
14249
14250        let program = Program::parse(initial_source).unwrap().0.unwrap();
14251
14252        let mut frontend = FrontendState::new();
14253
14254        let ctx = ExecutorContext::new_mock(None).await;
14255        let version = Version(0);
14256
14257        frontend.program = program.clone();
14258        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14259        frontend.update_state_after_exec(outcome, true);
14260        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14261        let sketch_id = sketch_object.id;
14262        let sketch = expect_sketch(sketch_object);
14263        let arc1_id = *sketch.segments.get(3).unwrap();
14264        let arc2_id = *sketch.segments.get(7).unwrap();
14265        let axis_id = *sketch.segments.get(10).unwrap();
14266
14267        let constraint = Constraint::Symmetric(Symmetric {
14268            input: vec![arc1_id, arc2_id],
14269            axis: axis_id,
14270        });
14271        let (src_delta, scene_delta) = frontend
14272            .add_constraint(&ctx, version, sketch_id, constraint)
14273            .await
14274            .unwrap();
14275        insta::assert_snapshot!("test_segments_symmetric_arcs", src_delta.text.as_str());
14276        assert_eq!(
14277            scene_delta.new_graph.objects.len(),
14278            14,
14279            "{:#?}",
14280            scene_delta.new_graph.objects
14281        );
14282
14283        ctx.close().await;
14284    }
14285
14286    #[tokio::test(flavor = "multi_thread")]
14287    async fn test_sketch_on_face_simple() {
14288        let initial_source = "\
14289len = 2mm
14290cube = startSketchOn(XY)
14291  |> startProfile(at = [0, 0])
14292  |> line(end = [len, 0], tag = $side)
14293  |> line(end = [0, len])
14294  |> line(end = [-len, 0])
14295  |> line(end = [0, -len])
14296  |> close()
14297  |> extrude(length = len)
14298
14299face = faceOf(cube, face = side)
14300";
14301
14302        let program = Program::parse(initial_source).unwrap().0.unwrap();
14303
14304        let mut frontend = FrontendState::new();
14305
14306        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14307        let mock_ctx = ExecutorContext::new_mock(None).await;
14308        let version = Version(0);
14309
14310        frontend.hack_set_program(&ctx, program).await.unwrap();
14311        let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
14312        let face_id = face_object.id;
14313
14314        let sketch_args = SketchCtor {
14315            on: Plane::Object(face_id),
14316        };
14317        let (_src_delta, scene_delta, sketch_id) = frontend
14318            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14319            .await
14320            .unwrap();
14321        assert_eq!(sketch_id, ObjectId(2));
14322        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14323        let sketch_object = &scene_delta.new_graph.objects[2];
14324        assert_eq!(sketch_object.id, ObjectId(2));
14325        assert_eq!(
14326            sketch_object.kind,
14327            ObjectKind::Sketch(Sketch {
14328                args: SketchCtor {
14329                    on: Plane::Object(face_id),
14330                },
14331                plane: face_id,
14332                segments: vec![],
14333                constraints: vec![],
14334            })
14335        );
14336        assert_eq!(scene_delta.new_graph.objects.len(), 8);
14337
14338        ctx.close().await;
14339        mock_ctx.close().await;
14340    }
14341
14342    #[tokio::test(flavor = "multi_thread")]
14343    async fn test_new_sketch_on_primitive_index_face() {
14344        let initial_source = "\
14345@settings(kclVersion = 2.0)
14346
14347sketch001 = sketch(on = XY) {
14348  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
14349}
14350extrude001 = extrude(region(point = [0mm, 0mm], sketch = sketch001), length = 5, tagEnd = $capEnd001)
14351shell001 = shell(extrude001, faces = capEnd001, thickness = 1)";
14352        let program = Program::parse(initial_source).unwrap().0.unwrap();
14353        let ctx = ExecutorContext::new_mock(None).await;
14354        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14355        let solid_id = match outcome.variables.get("shell001") {
14356            Some(KclValueView::Solid { value }) => value.id,
14357            value => panic!("expected shell001 to be a solid, got {value:?}"),
14358        };
14359        let solid_references = solid_references_from_variables(&program.ast, &outcome.variables);
14360
14361        let mut ast = program.ast;
14362        let scene_graph = SceneGraph::empty(ProjectId(0), FileId(0), Version(0));
14363        let face_expr = sketch_on_ast_expr(
14364            &mut ast,
14365            &scene_graph,
14366            &solid_references,
14367            &Plane::PrimitiveFace(crate::frontend::api::PrimitiveFacePlane { solid_id, index: 6 }),
14368        )
14369        .unwrap();
14370        let face_decl = ast::VariableDeclaration::new(
14371            ast::VariableDeclarator::new("face001", face_expr),
14372            ast::ItemVisibility::Default,
14373            ast::VariableKind::Const,
14374        );
14375        ast.body
14376            .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
14377                face_decl,
14378            ))));
14379        let face_source = source_from_ast(&ast);
14380        let new_source = format!("{face_source}sketch002 = sketch(on = face001) {{\n}}\n");
14381        insta::assert_snapshot!("test_new_sketch_on_primitive_index_face", new_source);
14382
14383        let program = Program::parse(&new_source).unwrap().0.unwrap();
14384        ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14385        ctx.close().await;
14386    }
14387
14388    #[tokio::test(flavor = "multi_thread")]
14389    async fn test_sketch_on_wall_artifact_from_region_extrude() {
14390        let initial_source = "\
14391s = sketch(on = YZ) {
14392  line1 = line(start = [0, 0], end = [0, 1])
14393  line2 = line(start = [0, 1], end = [1, 1])
14394  line3 = line(start = [1, 1], end = [0, 0])
14395}
14396region001 = region(point = [0.1, 0.1], sketch = s)
14397extrude001 = extrude(region001, length = 5)
14398";
14399
14400        let program = Program::parse(initial_source).unwrap().0.unwrap();
14401
14402        let mut frontend = FrontendState::new();
14403        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14404        let version = Version(0);
14405
14406        frontend.hack_set_program(&ctx, program).await.unwrap();
14407        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
14408
14409        let sketch_args = SketchCtor {
14410            on: Plane::Object(wall_object_id),
14411        };
14412        let (src_delta, _scene_delta, _sketch_id) = frontend
14413            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14414            .await
14415            .unwrap();
14416        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
14417
14418        ctx.close().await;
14419    }
14420
14421    #[tokio::test(flavor = "multi_thread")]
14422    async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
14423        let initial_source = "\
14424sketch001 = sketch(on = YZ) {
14425  line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
14426  line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
14427  line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
14428  line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
14429  coincident([line1.end, line2.start])
14430  coincident([line2.end, line3.start])
14431  coincident([line3.end, line4.start])
14432  coincident([line4.end, line1.start])
14433  parallel([line2, line4])
14434  parallel([line3, line1])
14435  perpendicular([line1, line2])
14436  horizontal(line3)
14437  line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
14438}
14439region001 = region(point = [3.1, 3.74], sketch = sketch001)
14440extrude001 = extrude(region001, length = 5)
14441";
14442
14443        let program = Program::parse(initial_source).unwrap().0.unwrap();
14444
14445        let mut frontend = FrontendState::new();
14446        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14447        let version = Version(0);
14448
14449        frontend.hack_set_program(&ctx, program).await.unwrap();
14450        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
14451
14452        let sketch_args = SketchCtor {
14453            on: Plane::Object(wall_object_id),
14454        };
14455        let (src_delta, _scene_delta, _sketch_id) = frontend
14456            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14457            .await
14458            .unwrap();
14459        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
14460
14461        ctx.close().await;
14462    }
14463
14464    #[tokio::test(flavor = "multi_thread")]
14465    async fn test_new_sketch_on_multi_region_extrude_cap_indexes_selected_solid() {
14466        let initial_source = "\
14467@settings(kclVersion = 2.0)
14468
14469sketch001 = sketch(on = XY) {
14470  circle1 = circle(start = [var -1.51mm, var 1.31mm], center = [var -1.98mm, var 1.03mm])
14471  circle2 = circle(start = [var 2.98mm, var 2.37mm], center = [var 2.66mm, var 1.46mm])
14472}
14473hidden001 = hide(sketch001)
14474region001 = region(segments = [sketch001.circle2])
14475region002 = region(segments = [sketch001.circle1])
14476extrude001 = extrude([region001, region002], length = 5)
14477";
14478
14479        let program = Program::parse(initial_source).unwrap().0.unwrap();
14480        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14481        let version = Version(0);
14482
14483        for (solid_output_index, expected_face) in [
14484            (0, "faceOf(extrude001[0], face = END)"),
14485            (1, "faceOf(extrude001[1], face = END)"),
14486        ] {
14487            let mut frontend = FrontendState::new();
14488            frontend.hack_set_program(&ctx, program.clone()).await.unwrap();
14489            let cap_object_id = find_cap_object_id_with_solid_output_index(
14490                &frontend.scene_graph,
14491                crate::frontend::api::CapKind::End,
14492                solid_output_index,
14493            )
14494            .unwrap_or_else(|| panic!("expected an end cap object for solid output index {solid_output_index}"));
14495
14496            let sketch_args = SketchCtor {
14497                on: Plane::Object(cap_object_id),
14498            };
14499            let (src_delta, _scene_delta, _sketch_id) = frontend
14500                .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14501                .await
14502                .unwrap();
14503
14504            assert!(
14505                src_delta.text.contains(expected_face),
14506                "expected `{expected_face}` in:\n{}",
14507                src_delta.text
14508            );
14509            assert!(!src_delta.text.contains("faceOf(extrude001, face = END)"));
14510        }
14511
14512        ctx.close().await;
14513    }
14514
14515    #[tokio::test(flavor = "multi_thread")]
14516    async fn test_new_sketch_on_multi_region_extrude_wall_indexes_selected_solid() {
14517        let initial_source = "\
14518@settings(kclVersion = 2.0)
14519
14520sketch001 = sketch(on = XY) {
14521  rect1Line1 = line(start = [0, 0], end = [1, 0])
14522  rect1Line2 = line(start = [1, 0], end = [1, 1])
14523  rect1Line3 = line(start = [1, 1], end = [0, 1])
14524  rect1Line4 = line(start = [0, 1], end = [0, 0])
14525  rect2Line1 = line(start = [3, 0], end = [4, 0])
14526  rect2Line2 = line(start = [4, 0], end = [4, 1])
14527  rect2Line3 = line(start = [4, 1], end = [3, 1])
14528  rect2Line4 = line(start = [3, 1], end = [3, 0])
14529}
14530hidden001 = hide(sketch001)
14531region001 = region(segments = [
14532  sketch001.rect1Line4,
14533  sketch001.rect1Line1
14534])
14535region002 = region(segments = [
14536  sketch001.rect2Line4,
14537  sketch001.rect2Line1
14538])
14539extrude001 = extrude([region001, region002], length = 5)
14540";
14541
14542        let program = Program::parse(initial_source).unwrap().0.unwrap();
14543        let mut frontend = FrontendState::new();
14544        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14545        let version = Version(0);
14546
14547        frontend.hack_set_program(&ctx, program).await.unwrap();
14548        let region_call = "\
14549region(segments = [
14550  sketch001.rect1Line4,
14551  sketch001.rect1Line1
14552])";
14553        let region_call_start = initial_source.find(region_call).unwrap();
14554        let region_range = [region_call_start, region_call_start + region_call.len(), 0].into();
14555        let segment_call = "line(start = [0, 0], end = [1, 0])";
14556        let segment_call_start = initial_source.find(segment_call).unwrap();
14557        let segment_range = [segment_call_start, segment_call_start + segment_call.len(), 0].into();
14558        let wall_object_id = frontend
14559            .scene_graph
14560            .objects
14561            .iter()
14562            .find_map(|object| match &object.kind {
14563                ObjectKind::Wall(wall)
14564                    if wall.source.path.as_ref().is_some_and(|path| path.range == region_range)
14565                        && wall.source.segment.range == segment_range =>
14566                {
14567                    Some(object.id)
14568                }
14569                _ => None,
14570            })
14571            .expect("expected a wall object for region001.tags.rect1Line1");
14572
14573        let sketch_args = SketchCtor {
14574            on: Plane::Object(wall_object_id),
14575        };
14576        let (src_delta, _scene_delta, _sketch_id) = frontend
14577            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14578            .await
14579            .unwrap();
14580
14581        let expected_face = "faceOf(extrude001[0], face = region001.tags.rect1Line1)";
14582        assert!(
14583            src_delta.text.contains(expected_face),
14584            "expected `{expected_face}` in:\n{}",
14585            src_delta.text
14586        );
14587        assert!(!src_delta.text.contains("faceOf(extrude001, face ="));
14588
14589        ctx.close().await;
14590    }
14591
14592    #[test]
14593    fn test_enclosing_variable_fallback_skips_nested_sketch_items() {
14594        let source = "\
14595sketch001 = sketch(on = XY) {
14596  line(start = [0, 0], end = [1, 0])
14597}
14598part = subtract(boxSolid, tools = [cutSolid])
14599  |> appearance(color = \"#8f96a3\")
14600";
14601        let ast = Program::parse(source).unwrap().0.unwrap().ast;
14602        let line_start = source.find("line").unwrap();
14603        let line_end = line_start + "line(start = [0, 0], end = [1, 0])".len();
14604        let line_ref = SourceRef::Simple {
14605            range: [line_start, line_end, 0].into(),
14606            node_path: None,
14607        };
14608        assert_eq!(variable_name_containing_source_ref(&ast, &line_ref), None);
14609
14610        let subtract_start = source.find("subtract").unwrap();
14611        let subtract_end = subtract_start + "subtract(boxSolid, tools = [cutSolid])".len();
14612        let subtract_ref = SourceRef::Simple {
14613            range: [subtract_start, subtract_end, 0].into(),
14614            node_path: None,
14615        };
14616        assert_eq!(
14617            variable_name_containing_source_ref(&ast, &subtract_ref),
14618            Some("part".to_owned())
14619        );
14620    }
14621
14622    #[tokio::test(flavor = "multi_thread")]
14623    async fn test_sketch_on_subtracted_sweep_cap_uses_composite_solid() {
14624        clear_mem_cache().await;
14625        let source = "\
14626boxSolid = startSketchOn(XY)
14627  |> startProfile(at = [0, 0])
14628  |> line(end = [4, 0], tag = $bottomEdge)
14629  |> line(end = [0, 4])
14630  |> line(end = [-4, 0])
14631  |> close()
14632  |> extrude(length = 10)
14633cutSolid = startSketchOn(XY)
14634  |> startProfile(at = [1, 1])
14635  |> line(end = [1, 0])
14636  |> line(end = [0, 1])
14637  |> line(end = [-1, 0])
14638  |> close()
14639  |> extrude(length = 10)
14640part = subtract(boxSolid, tools = [cutSolid])
14641  |> appearance(color = \"#8f96a3\", roughness = 55, metalness = 8)
14642";
14643        let program = Program::parse(source).unwrap().0.unwrap();
14644        let mut frontend = FrontendState::new();
14645        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14646        match frontend.hack_set_program(&ctx, program).await.unwrap() {
14647            SetProgramOutcome::Success { .. } => {}
14648            SetProgramOutcome::ExecFailure { error } => panic!("KCL fixture failed to execute: {error:?}"),
14649        }
14650
14651        let sweep_call_start = source.find("extrude").unwrap();
14652        let sweep_call_end = sweep_call_start + "extrude(length = 10)".len();
14653        let part_call_start = source.find("subtract").unwrap();
14654        let part_call_end = part_call_start + "subtract(boxSolid, tools = [cutSolid])".len();
14655        let sweep_range = [sweep_call_start, sweep_call_end, 0].into();
14656        let composite_range = [part_call_start, part_call_end, 0].into();
14657
14658        let cap_object = frontend
14659            .scene_graph
14660            .objects
14661            .iter()
14662            .find(|object| {
14663                matches!(
14664                    &object.kind,
14665                    ObjectKind::Cap(crate::frontend::api::Cap {
14666                        kind: crate::frontend::api::CapKind::End,
14667                        source,
14668                        ..
14669                    }) if source.solid.range == composite_range && source.sweep.range == sweep_range
14670                )
14671            })
14672            .expect("expected end cap object to trace through subtract and original extrude");
14673
14674        let mut ast = frontend.program.ast.clone();
14675        let cap_expr = sketch_on_ast_expr(
14676            &mut ast,
14677            &frontend.scene_graph,
14678            &frontend.solid_references,
14679            &Plane::Object(cap_object.id),
14680        )
14681        .unwrap();
14682        let cap_face_decl = ast::VariableDeclaration::new(
14683            ast::VariableDeclarator::new("capFace", cap_expr.clone()),
14684            ast::ItemVisibility::Default,
14685            ast::VariableKind::Const,
14686        );
14687        ast.body
14688            .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
14689                cap_face_decl,
14690            ))));
14691        let generated_source = source_from_ast(&ast);
14692
14693        assert!(generated_source.contains("capFace = faceOf(part, face = END)"));
14694        assert!(!generated_source.contains("faceOf(boxSolid"));
14695        let ast::Expr::CallExpressionKw(call) = cap_expr else {
14696            panic!("expected faceOf call");
14697        };
14698        assert_eq!(call.callee.name.name, "faceOf");
14699        let ast::Expr::Name(solid_name) = call.unlabeled.as_ref().unwrap() else {
14700            panic!("expected solid name");
14701        };
14702        assert_eq!(solid_name.name.name, "part");
14703        let ast::Expr::Name(face_name) = &call.arguments[0].arg else {
14704            panic!("expected face name");
14705        };
14706        assert_eq!(face_name.name.name, "END");
14707
14708        ctx.close().await;
14709    }
14710
14711    #[tokio::test(flavor = "multi_thread")]
14712    async fn test_sketch_on_plane_incremental() {
14713        let initial_source = "\
14714len = 2mm
14715cube = startSketchOn(XY)
14716  |> startProfile(at = [0, 0])
14717  |> line(end = [len, 0], tag = $side)
14718  |> line(end = [0, len])
14719  |> line(end = [-len, 0])
14720  |> line(end = [0, -len])
14721  |> close()
14722  |> extrude(length = len)
14723
14724plane = planeOf(cube, face = side)
14725";
14726
14727        let program = Program::parse(initial_source).unwrap().0.unwrap();
14728
14729        let mut frontend = FrontendState::new();
14730
14731        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14732        let mock_ctx = ExecutorContext::new_mock(None).await;
14733        let version = Version(0);
14734
14735        frontend.hack_set_program(&ctx, program).await.unwrap();
14736        // Find the last plane since the first plane is the XY plane.
14737        let plane_object = frontend
14738            .scene_graph
14739            .objects
14740            .iter()
14741            .rev()
14742            .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
14743            .unwrap();
14744        let plane_id = plane_object.id;
14745
14746        let sketch_args = SketchCtor {
14747            on: Plane::Object(plane_id),
14748        };
14749        let (src_delta, scene_delta, sketch_id) = frontend
14750            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14751            .await
14752            .unwrap();
14753        insta::assert_snapshot!("test_sketch_on_plane_incremental", src_delta.text.as_str());
14754        assert_eq!(sketch_id, ObjectId(2));
14755        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14756        let sketch_object = &scene_delta.new_graph.objects[2];
14757        assert_eq!(sketch_object.id, ObjectId(2));
14758        assert_eq!(
14759            sketch_object.kind,
14760            ObjectKind::Sketch(Sketch {
14761                args: SketchCtor {
14762                    on: Plane::Object(plane_id),
14763                },
14764                plane: plane_id,
14765                segments: vec![],
14766                constraints: vec![],
14767            })
14768        );
14769        assert_eq!(scene_delta.new_graph.objects.len(), 9);
14770
14771        let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
14772        assert_eq!(plane_object.id, plane_id);
14773        assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
14774
14775        ctx.close().await;
14776        mock_ctx.close().await;
14777    }
14778
14779    #[tokio::test(flavor = "multi_thread")]
14780    async fn test_new_sketch_uses_unique_variable_name() {
14781        let initial_source = "\
14782sketch1 = sketch(on = XY) {
14783}
14784";
14785
14786        let program = Program::parse(initial_source).unwrap().0.unwrap();
14787
14788        let mut frontend = FrontendState::new();
14789        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14790        let version = Version(0);
14791
14792        frontend.hack_set_program(&ctx, program).await.unwrap();
14793
14794        let sketch_args = SketchCtor {
14795            on: Plane::Default(PlaneName::Yz),
14796        };
14797        let (src_delta, _, _) = frontend
14798            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14799            .await
14800            .unwrap();
14801
14802        insta::assert_snapshot!("test_new_sketch_uses_unique_variable_name", src_delta.text.as_str());
14803
14804        ctx.close().await;
14805    }
14806
14807    #[tokio::test(flavor = "multi_thread")]
14808    async fn test_new_sketch_twice_using_same_plane() {
14809        let initial_source = "\
14810sketch1 = sketch(on = XY) {
14811}
14812";
14813
14814        let program = Program::parse(initial_source).unwrap().0.unwrap();
14815
14816        let mut frontend = FrontendState::new();
14817        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14818        let version = Version(0);
14819
14820        frontend.hack_set_program(&ctx, program).await.unwrap();
14821
14822        let sketch_args = SketchCtor {
14823            on: Plane::Default(PlaneName::Xy),
14824        };
14825        let (src_delta, _, _) = frontend
14826            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14827            .await
14828            .unwrap();
14829
14830        insta::assert_snapshot!("test_new_sketch_twice_using_same_plane", src_delta.text.as_str());
14831
14832        ctx.close().await;
14833    }
14834
14835    #[tokio::test(flavor = "multi_thread")]
14836    async fn test_sketch_mode_reuses_cached_on_expression() {
14837        let initial_source = "\
14838width = 2mm
14839sketch(on = offsetPlane(XY, offset = width)) {
14840  line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
14841  distance([line1.start, line1.end]) == width
14842}
14843";
14844        let program = Program::parse(initial_source).unwrap().0.unwrap();
14845
14846        let mut frontend = FrontendState::new();
14847        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14848        let mock_ctx = ExecutorContext::new_mock(None).await;
14849        let version = Version(0);
14850        let project_id = ProjectId(0);
14851        let file_id = FileId(0);
14852
14853        frontend.hack_set_program(&ctx, program).await.unwrap();
14854        let initial_object_count = frontend.scene_graph.objects.len();
14855        let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14856            .expect("Expected sketch object to exist")
14857            .id;
14858
14859        // Entering sketch mode should reuse cached `on` expression state
14860        // (offsetPlane result), not fail or create extra on-surface objects.
14861        let scene_delta = frontend
14862            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14863            .await
14864            .unwrap();
14865        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14866
14867        // A follow-up sketch-mode execution should keep the same stable object
14868        // graph shape as well.
14869        let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
14870        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14871
14872        ctx.close().await;
14873        mock_ctx.close().await;
14874    }
14875
14876    #[tokio::test(flavor = "multi_thread")]
14877    async fn test_edit_sketch_nested_in_pipe() {
14878        clear_mem_cache().await;
14879        let source = r#"
14880profile = sketch(on = XY) {
14881  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
14882}
14883  |> translate(x = 2mm)
14884"#;
14885        let program = Program::parse_no_errs(source).unwrap();
14886        let mut frontend = FrontendState::new();
14887        let mock_ctx = ExecutorContext::new_mock(None).await;
14888        let version = Version(0);
14889
14890        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
14891        let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14892            .expect("Expected piped sketch object")
14893            .id;
14894
14895        let scene_delta = frontend
14896            .edit_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_id)
14897            .await
14898            .unwrap();
14899        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch_id));
14900        assert!(
14901            scene_delta
14902                .new_graph
14903                .objects
14904                .iter()
14905                .any(|object| matches!(&object.kind, ObjectKind::Segment { .. })),
14906            "Expected the piped sketch's segments to be present in sketch mode"
14907        );
14908
14909        clear_mem_cache().await;
14910        mock_ctx.close().await;
14911    }
14912
14913    #[tokio::test(flavor = "multi_thread")]
14914    async fn test_issue_9409_edit_sketch_nested_in_if_with_var_feedback() {
14915        clear_mem_cache().await;
14916        let source = r#"
14917useFirstProfile = true
14918
14919profile = if useFirstProfile {
14920  sketch(on = XY) {
14921    line1 = line(start = [0mm, 0mm], end = [var 20mm, var 10mm])
14922  }
14923} else {
14924  sketch(on = XY) {
14925    line2 = line(start = [0mm, 0mm], end = [var 10mm, var 20mm])
14926  }
14927}
14928"#;
14929        let program = Program::parse_no_errs(source).unwrap();
14930        let mut frontend = FrontendState::new();
14931        let mock_ctx = ExecutorContext::new_mock(None).await;
14932        let version = Version(0);
14933
14934        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
14935        let sketch_object =
14936            find_first_sketch_object(&frontend.scene_graph).expect("Expected active branch's sketch object");
14937        let sketch_id = sketch_object.id;
14938        let sketch = expect_sketch(sketch_object);
14939        let line_end_id = *sketch
14940            .segments
14941            .get(1)
14942            .expect("Expected the active branch's line end point");
14943
14944        let scene_delta = frontend
14945            .edit_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_id)
14946            .await
14947            .unwrap();
14948        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch_id));
14949
14950        let segments = vec![ExistingSegmentCtor {
14951            id: line_end_id,
14952            ctor: SegmentCtor::Point(PointCtor {
14953                position: Point2d {
14954                    x: Expr::Var(Number {
14955                        value: 30.0,
14956                        units: NumericSuffix::Mm,
14957                    }),
14958                    y: Expr::Var(Number {
14959                        value: 15.0,
14960                        units: NumericSuffix::Mm,
14961                    }),
14962                },
14963            }),
14964        }];
14965        let (source_delta, _) = frontend
14966            .edit_segments(&mock_ctx, version, sketch_id, segments)
14967            .await
14968            .unwrap();
14969        assert!(
14970            source_delta
14971                .text
14972                .contains("line1 = line(start = [0mm, 0mm], end = [var 30mm, var 15mm])"),
14973            "Expected the active branch's dragged variables to be updated:\n{}",
14974            source_delta.text
14975        );
14976        assert!(
14977            source_delta
14978                .text
14979                .contains("line2 = line(start = [0mm, 0mm], end = [var 10mm, var 20mm])"),
14980            "Expected the inactive branch to remain unchanged:\n{}",
14981            source_delta.text
14982        );
14983
14984        clear_mem_cache().await;
14985        mock_ctx.close().await;
14986    }
14987
14988    #[tokio::test(flavor = "multi_thread")]
14989    async fn test_multiple_sketch_blocks() {
14990        let initial_source = "\
14991// Cube that requires the engine.
14992width = 2
14993sketch001 = startSketchOn(XY)
14994profile001 = startProfile(sketch001, at = [0, 0])
14995  |> yLine(length = width, tag = $seg1)
14996  |> xLine(length = width)
14997  |> yLine(length = -width)
14998  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14999  |> close()
15000extrude001 = extrude(profile001, length = width)
15001
15002// Get a value that requires the engine.
15003x = segLen(seg1)
15004
15005// Triangle with side length 2*x.
15006sketch(on = XY) {
15007  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
15008  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
15009  coincident([line1.end, line2.start])
15010  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
15011  coincident([line2.end, line3.start])
15012  coincident([line3.end, line1.start])
15013  equalLength([line3, line1])
15014  equalLength([line1, line2])
15015  distance([line1.start, line1.end]) == 2*x
15016}
15017
15018// Line segment with length x.
15019sketch2 = sketch(on = XY) {
15020  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
15021  distance([line1.start, line1.end]) == x
15022}
15023";
15024
15025        let program = Program::parse(initial_source).unwrap().0.unwrap();
15026
15027        let mut frontend = FrontendState::new();
15028
15029        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15030        let mock_ctx = ExecutorContext::new_mock(None).await;
15031        let version = Version(0);
15032        let project_id = ProjectId(0);
15033        let file_id = FileId(0);
15034
15035        frontend.hack_set_program(&ctx, program).await.unwrap();
15036        let sketch_objects = frontend
15037            .scene_graph
15038            .objects
15039            .iter()
15040            .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
15041            .collect::<Vec<_>>();
15042        let sketch1_id = sketch_objects.first().unwrap().id;
15043        let sketch2_id = sketch_objects.get(1).unwrap().id;
15044        // First point in sketch1.
15045        let point1_id = ObjectId(sketch1_id.0 + 1);
15046        // First point in sketch2.
15047        let point2_id = ObjectId(sketch2_id.0 + 1);
15048
15049        // Edit the first sketch. Objects before the sketch block should be
15050        // present from execution cache so that we can sketch on prior planes,
15051        // for example. Objects after the first sketch block should not be
15052        // present since those statements are skipped in sketch mode.
15053        //
15054        // - startSketchOn(XY) Plane 1
15055        // - sketch on=XY Plane 1
15056        // - Sketch block 16
15057        let scene_delta = frontend
15058            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
15059            .await
15060            .unwrap();
15061        assert_eq!(
15062            scene_delta.new_graph.objects.len(),
15063            18,
15064            "{:#?}",
15065            scene_delta.new_graph.objects
15066        );
15067
15068        // Edit a point in the first sketch.
15069        let point_ctor = PointCtor {
15070            position: Point2d {
15071                x: Expr::Var(Number {
15072                    value: 1.0,
15073                    units: NumericSuffix::Mm,
15074                }),
15075                y: Expr::Var(Number {
15076                    value: 2.0,
15077                    units: NumericSuffix::Mm,
15078                }),
15079            },
15080        };
15081        let segments = vec![ExistingSegmentCtor {
15082            id: point1_id,
15083            ctor: SegmentCtor::Point(point_ctor),
15084        }];
15085        let (src_delta, _) = frontend
15086            .edit_segments(&mock_ctx, version, sketch1_id, segments)
15087            .await
15088            .unwrap();
15089        // Only the first sketch block changes.
15090        insta::assert_snapshot!("test_multiple_sketch_blocks_1", src_delta.text.as_str());
15091        let edited_sketch1_source = src_delta.text.clone();
15092
15093        // Execute mock to simulate drag end.
15094        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
15095        assert_eq!(src_delta.text, edited_sketch1_source);
15096        // Exit sketch. Objects from the entire program should be present.
15097        //
15098        // - startSketchOn(XY) Plane 1
15099        // - sketch on=XY Plane 1
15100        // - Sketch block 16
15101        // - sketch on=XY cached
15102        // - Sketch block 5
15103        let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
15104        assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
15105
15106        // Edit the second sketch.
15107        //
15108        // - startSketchOn(XY) Plane 1
15109        // - sketch on=XY Plane 1
15110        // - Sketch block 16
15111        // - sketch on=XY cached
15112        // - Sketch block 5
15113        let scene_delta = frontend
15114            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
15115            .await
15116            .unwrap();
15117        assert_eq!(
15118            scene_delta.new_graph.objects.len(),
15119            24,
15120            "{:#?}",
15121            scene_delta.new_graph.objects
15122        );
15123
15124        // Edit a point in the second sketch.
15125        let point_ctor = PointCtor {
15126            position: Point2d {
15127                x: Expr::Var(Number {
15128                    value: 3.0,
15129                    units: NumericSuffix::Mm,
15130                }),
15131                y: Expr::Var(Number {
15132                    value: 4.0,
15133                    units: NumericSuffix::Mm,
15134                }),
15135            },
15136        };
15137        let segments = vec![ExistingSegmentCtor {
15138            id: point2_id,
15139            ctor: SegmentCtor::Point(point_ctor),
15140        }];
15141        let (src_delta, _) = frontend
15142            .edit_segments(&mock_ctx, version, sketch2_id, segments)
15143            .await
15144            .unwrap();
15145        // Only the second sketch block changes.
15146        insta::assert_snapshot!("test_multiple_sketch_blocks_2", src_delta.text.as_str());
15147        let edited_sketch2_source = src_delta.text.clone();
15148
15149        // Execute mock to simulate drag end.
15150        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
15151        assert_eq!(src_delta.text, edited_sketch2_source);
15152
15153        ctx.close().await;
15154        mock_ctx.close().await;
15155    }
15156
15157    #[tokio::test(flavor = "multi_thread")]
15158    async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
15159        clear_mem_cache().await;
15160
15161        let source = r#"sketch001 = sketch(on = XZ) {
15162  circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
15163}
15164sketch002 = sketch(on = XY) {
15165  line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
15166  line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
15167  line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
15168  line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
15169  coincident([line1.end, line2.start])
15170  coincident([line2.end, line3.start])
15171  coincident([line3.end, line4.start])
15172  coincident([line4.end, line1.start])
15173  parallel([line2, line4])
15174  parallel([line3, line1])
15175  perpendicular([line1, line2])
15176  horizontal(line3)
15177  coincident([line1.start, ORIGIN])
15178}
15179"#;
15180
15181        let program = Program::parse(source).unwrap().0.unwrap();
15182        let mut frontend = FrontendState::new();
15183        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
15184        let mock_ctx = ExecutorContext::new_mock(None).await;
15185        let version = Version(0);
15186        let project_id = ProjectId(0);
15187        let file_id = FileId(0);
15188
15189        frontend.hack_set_program(&ctx, program).await.unwrap();
15190        let sketch_objects = frontend
15191            .scene_graph
15192            .objects
15193            .iter()
15194            .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
15195            .collect::<Vec<_>>();
15196        assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
15197
15198        let sketch1_id = sketch_objects[0].id;
15199        let sketch2_id = sketch_objects[1].id;
15200
15201        frontend
15202            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
15203            .await
15204            .unwrap();
15205        frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
15206
15207        let scene_delta = frontend
15208            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
15209            .await
15210            .unwrap();
15211        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
15212
15213        clear_mem_cache().await;
15214        ctx.close().await;
15215        mock_ctx.close().await;
15216    }
15217
15218    // Regression tests: operations on source code with extra whitespace/newlines.
15219    // These test that NodePath-based lookups work correctly when source ranges
15220    // are shifted by extra whitespace that wouldn't be present after formatting.
15221
15222    #[tokio::test(flavor = "multi_thread")]
15223    async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
15224        // Extra newlines after @settings line - this shifts all source ranges.
15225        let initial_source = "@settings(defaultLengthUnit = mm)
15226
15227sketch001 = sketch(on = XY) {
15228  point(at = [1in, 2in])
15229}
15230";
15231
15232        let program = Program::parse(initial_source).unwrap().0.unwrap();
15233        let mut frontend = FrontendState::new();
15234
15235        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15236        let mock_ctx = ExecutorContext::new_mock(None).await;
15237        let version = Version(0);
15238        let project_id = ProjectId(0);
15239        let file_id = FileId(0);
15240
15241        frontend.hack_set_program(&ctx, program).await.unwrap();
15242        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15243        let sketch_id = sketch_object.id;
15244
15245        // Edit sketch should succeed despite extra newlines.
15246        frontend
15247            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15248            .await
15249            .unwrap();
15250
15251        // Add a new point to the sketch.
15252        let point_ctor = PointCtor {
15253            position: Point2d {
15254                x: Expr::Number(Number {
15255                    value: 5.0,
15256                    units: NumericSuffix::Mm,
15257                }),
15258                y: Expr::Number(Number {
15259                    value: 6.0,
15260                    units: NumericSuffix::Mm,
15261                }),
15262            },
15263        };
15264        let segment = SegmentCtor::Point(point_ctor);
15265        let (src_delta, scene_delta) = frontend
15266            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15267            .await
15268            .unwrap();
15269        // After adding a point, the source should be reformatted with standard whitespace.
15270        assert!(
15271            src_delta.text.contains("point(at = [5mm, 6mm])"),
15272            "Expected new point in source, got: {}",
15273            src_delta.text
15274        );
15275        assert!(!scene_delta.new_objects.is_empty());
15276
15277        ctx.close().await;
15278        mock_ctx.close().await;
15279    }
15280
15281    #[tokio::test(flavor = "multi_thread")]
15282    async fn test_ensure_control_point_spline_experimental_features_adds_allow_setting() {
15283        let initial_program = Program::parse("s = sketch(on = XY) {}\n").unwrap().0.unwrap();
15284
15285        let updated_program = ensure_control_point_spline_experimental_features(&initial_program).unwrap();
15286        let meta_settings = updated_program.meta_settings().unwrap().unwrap();
15287
15288        assert_eq!(meta_settings.experimental_features, WarningLevel::Allow);
15289        assert!(
15290            source_from_ast(&updated_program.ast).contains("@settings(experimentalFeatures = allow)"),
15291            "Expected experimental settings to be added to source"
15292        );
15293    }
15294
15295    #[tokio::test(flavor = "multi_thread")]
15296    async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
15297        // Extra newlines after @settings, with an empty sketch block.
15298        let initial_source = "@settings(defaultLengthUnit = mm)
15299
15300s = sketch(on = XY) {}
15301";
15302
15303        let program = Program::parse(initial_source).unwrap().0.unwrap();
15304        let mut frontend = FrontendState::new();
15305
15306        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15307        let mock_ctx = ExecutorContext::new_mock(None).await;
15308        let version = Version(0);
15309
15310        frontend.hack_set_program(&ctx, program).await.unwrap();
15311        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15312        let sketch_id = sketch_object.id;
15313
15314        let line_ctor = LineCtor {
15315            start: Point2d {
15316                x: Expr::Number(Number {
15317                    value: 0.0,
15318                    units: NumericSuffix::Mm,
15319                }),
15320                y: Expr::Number(Number {
15321                    value: 0.0,
15322                    units: NumericSuffix::Mm,
15323                }),
15324            },
15325            end: Point2d {
15326                x: Expr::Number(Number {
15327                    value: 10.0,
15328                    units: NumericSuffix::Mm,
15329                }),
15330                y: Expr::Number(Number {
15331                    value: 10.0,
15332                    units: NumericSuffix::Mm,
15333                }),
15334            },
15335            construction: None,
15336        };
15337        let segment = SegmentCtor::Line(line_ctor);
15338        let (src_delta, scene_delta) = frontend
15339            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15340            .await
15341            .unwrap();
15342        assert!(
15343            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15344            "Expected line in source, got: {}",
15345            src_delta.text
15346        );
15347        // Line creates start point, end point, and line segment.
15348        assert_eq!(scene_delta.new_objects.len(), 3);
15349
15350        ctx.close().await;
15351        mock_ctx.close().await;
15352    }
15353
15354    #[tokio::test(flavor = "multi_thread")]
15355    async fn test_extra_newlines_between_operations_edit_line() {
15356        // Extra newlines between @settings and sketch, and inside the sketch block.
15357        let initial_source = "@settings(defaultLengthUnit = mm)
15358
15359sketch001 = sketch(on = XY) {
15360
15361  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
15362
15363}
15364";
15365
15366        let program = Program::parse(initial_source).unwrap().0.unwrap();
15367        let mut frontend = FrontendState::new();
15368
15369        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15370        let mock_ctx = ExecutorContext::new_mock(None).await;
15371        let version = Version(0);
15372        let project_id = ProjectId(0);
15373        let file_id = FileId(0);
15374
15375        frontend.hack_set_program(&ctx, program).await.unwrap();
15376        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15377        let sketch_id = sketch_object.id;
15378        let sketch = expect_sketch(sketch_object);
15379
15380        // Extract segment IDs before edit_sketch borrows frontend mutably.
15381        let line_id = sketch
15382            .segments
15383            .iter()
15384            .copied()
15385            .find(|seg_id| {
15386                matches!(
15387                    &frontend.scene_graph.objects[seg_id.0].kind,
15388                    ObjectKind::Segment {
15389                        segment: Segment::Line(_)
15390                    }
15391                )
15392            })
15393            .expect("Expected a line segment in sketch");
15394
15395        // Enter sketch edit mode.
15396        frontend
15397            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15398            .await
15399            .unwrap();
15400
15401        // Edit the line.
15402        let line_ctor = LineCtor {
15403            start: Point2d {
15404                x: Expr::Var(Number {
15405                    value: 1.0,
15406                    units: NumericSuffix::Mm,
15407                }),
15408                y: Expr::Var(Number {
15409                    value: 2.0,
15410                    units: NumericSuffix::Mm,
15411                }),
15412            },
15413            end: Point2d {
15414                x: Expr::Var(Number {
15415                    value: 13.0,
15416                    units: NumericSuffix::Mm,
15417                }),
15418                y: Expr::Var(Number {
15419                    value: 14.0,
15420                    units: NumericSuffix::Mm,
15421                }),
15422            },
15423            construction: None,
15424        };
15425        let segments = vec![ExistingSegmentCtor {
15426            id: line_id,
15427            ctor: SegmentCtor::Line(line_ctor),
15428        }];
15429        let (src_delta, _scene_delta) = frontend
15430            .edit_segments(&mock_ctx, version, sketch_id, segments)
15431            .await
15432            .unwrap();
15433        assert!(
15434            src_delta
15435                .text
15436                .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
15437            "Expected edited line in source, got: {}",
15438            src_delta.text
15439        );
15440
15441        ctx.close().await;
15442        mock_ctx.close().await;
15443    }
15444
15445    #[tokio::test(flavor = "multi_thread")]
15446    async fn test_extra_newlines_delete_segment() {
15447        // Extra whitespace before and after the sketch block.
15448        let initial_source = "@settings(defaultLengthUnit = mm)
15449
15450sketch001 = sketch(on = XY) {
15451  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
15452}
15453";
15454
15455        let program = Program::parse(initial_source).unwrap().0.unwrap();
15456        let mut frontend = FrontendState::new();
15457
15458        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15459        let mock_ctx = ExecutorContext::new_mock(None).await;
15460        let version = Version(0);
15461
15462        frontend.hack_set_program(&ctx, program).await.unwrap();
15463        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15464        let sketch_id = sketch_object.id;
15465        let sketch = expect_sketch(sketch_object);
15466
15467        // The sketch should have 3 segments: start point, center point, and the circle.
15468        assert_eq!(sketch.segments.len(), 3);
15469        let circle_id = sketch.segments[2];
15470
15471        // Delete the circle despite extra newlines in original source.
15472        let (src_delta, scene_delta) = frontend
15473            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
15474            .await
15475            .unwrap();
15476        assert!(
15477            src_delta.text.contains("sketch(on = XY) {"),
15478            "Expected sketch block in source, got: {}",
15479            src_delta.text
15480        );
15481        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
15482        let new_sketch = expect_sketch(new_sketch_object);
15483        assert_eq!(new_sketch.segments.len(), 0);
15484
15485        ctx.close().await;
15486        mock_ctx.close().await;
15487    }
15488
15489    #[tokio::test(flavor = "multi_thread")]
15490    async fn test_unformatted_source_add_arc() {
15491        // Source with inconsistent whitespace - tabs, extra spaces, multiple blank lines.
15492        let initial_source = "@settings(defaultLengthUnit = mm)
15493
15494sketch001 = sketch(on = XY) {
15495}
15496";
15497
15498        let program = Program::parse(initial_source).unwrap().0.unwrap();
15499        let mut frontend = FrontendState::new();
15500
15501        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15502        let mock_ctx = ExecutorContext::new_mock(None).await;
15503        let version = Version(0);
15504
15505        frontend.hack_set_program(&ctx, program).await.unwrap();
15506        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15507        let sketch_id = sketch_object.id;
15508
15509        let arc_ctor = ArcCtor {
15510            start: Point2d {
15511                x: Expr::Var(Number {
15512                    value: 5.0,
15513                    units: NumericSuffix::Mm,
15514                }),
15515                y: Expr::Var(Number {
15516                    value: 0.0,
15517                    units: NumericSuffix::Mm,
15518                }),
15519            },
15520            end: Point2d {
15521                x: Expr::Var(Number {
15522                    value: 0.0,
15523                    units: NumericSuffix::Mm,
15524                }),
15525                y: Expr::Var(Number {
15526                    value: 5.0,
15527                    units: NumericSuffix::Mm,
15528                }),
15529            },
15530            center: Point2d {
15531                x: Expr::Var(Number {
15532                    value: 0.0,
15533                    units: NumericSuffix::Mm,
15534                }),
15535                y: Expr::Var(Number {
15536                    value: 0.0,
15537                    units: NumericSuffix::Mm,
15538                }),
15539            },
15540            direction: None,
15541            construction: None,
15542        };
15543        let segment = SegmentCtor::Arc(arc_ctor);
15544        let (src_delta, scene_delta) = frontend
15545            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15546            .await
15547            .unwrap();
15548        assert!(
15549            src_delta
15550                .text
15551                .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
15552            "Expected arc in source, got: {}",
15553            src_delta.text
15554        );
15555        assert!(!scene_delta.new_objects.is_empty());
15556
15557        ctx.close().await;
15558        mock_ctx.close().await;
15559    }
15560
15561    #[tokio::test(flavor = "multi_thread")]
15562    async fn test_arc_direction_flows_to_source() {
15563        let initial_source = "@settings(defaultLengthUnit = mm)
15564
15565sketch001 = sketch(on = XY) {
15566}
15567";
15568
15569        let program = Program::parse(initial_source).unwrap().0.unwrap();
15570        let mut frontend = FrontendState::new();
15571
15572        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15573        let mock_ctx = ExecutorContext::new_mock(None).await;
15574        let version = Version(0);
15575
15576        frontend.hack_set_program(&ctx, program).await.unwrap();
15577        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15578        let sketch_id = sketch_object.id;
15579
15580        let point = |x: f64, y: f64| Point2d {
15581            x: Expr::Var(Number {
15582                value: x,
15583                units: NumericSuffix::Mm,
15584            }),
15585            y: Expr::Var(Number {
15586                value: y,
15587                units: NumericSuffix::Mm,
15588            }),
15589        };
15590
15591        // Adding a clockwise arc writes its direction to the source.
15592        let arc_ctor = ArcCtor {
15593            start: point(5.0, 0.0),
15594            end: point(0.0, 5.0),
15595            center: point(0.0, 0.0),
15596            direction: Some(ArcDirection::Cw),
15597            construction: None,
15598        };
15599        let (src_delta, scene_delta) = frontend
15600            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
15601            .await
15602            .unwrap();
15603        assert!(
15604            src_delta.text.contains("direction = CW"),
15605            "Expected direction = CW in source, got: {}",
15606            src_delta.text
15607        );
15608        // The new objects are the end points, the center, and then the arc.
15609        let arc_id = *scene_delta.new_objects.last().unwrap();
15610
15611        // Editing the arc's points preserves the clockwise direction. The
15612        // edited points keep the same distance to the center so that the
15613        // solver doesn't need to move anything.
15614        let edited_ctor = ArcCtor {
15615            start: point(0.0, -5.0),
15616            end: point(0.0, 5.0),
15617            center: point(0.0, 0.0),
15618            direction: Some(ArcDirection::Cw),
15619            construction: None,
15620        };
15621        let (src_delta, _scene_delta) = frontend
15622            .edit_segments(
15623                &mock_ctx,
15624                version,
15625                sketch_id,
15626                vec![ExistingSegmentCtor {
15627                    id: arc_id,
15628                    ctor: SegmentCtor::Arc(edited_ctor),
15629                }],
15630            )
15631            .await
15632            .unwrap();
15633        assert!(
15634            src_delta.text.contains("start = [var 0mm, var -5mm]"),
15635            "Expected edited start point in source, got: {}",
15636            src_delta.text
15637        );
15638        assert!(
15639            src_delta.text.contains("direction = CW"),
15640            "Expected direction = CW to be preserved in source, got: {}",
15641            src_delta.text
15642        );
15643
15644        // Editing the arc back to counterclockwise removes the direction
15645        // argument since counterclockwise is the default.
15646        let edited_ctor = ArcCtor {
15647            start: point(0.0, -5.0),
15648            end: point(0.0, 5.0),
15649            center: point(0.0, 0.0),
15650            direction: Some(ArcDirection::Ccw),
15651            construction: None,
15652        };
15653        let (src_delta, _scene_delta) = frontend
15654            .edit_segments(
15655                &mock_ctx,
15656                version,
15657                sketch_id,
15658                vec![ExistingSegmentCtor {
15659                    id: arc_id,
15660                    ctor: SegmentCtor::Arc(edited_ctor),
15661                }],
15662            )
15663            .await
15664            .unwrap();
15665        assert!(
15666            !src_delta.text.contains("direction"),
15667            "Expected direction argument to be removed from source, got: {}",
15668            src_delta.text
15669        );
15670
15671        ctx.close().await;
15672        mock_ctx.close().await;
15673    }
15674
15675    #[tokio::test(flavor = "multi_thread")]
15676    async fn test_extra_newlines_add_circle() {
15677        // Extra blank lines between settings and sketch.
15678        let initial_source = "@settings(defaultLengthUnit = mm)
15679
15680sketch001 = sketch(on = XY) {
15681}
15682";
15683
15684        let program = Program::parse(initial_source).unwrap().0.unwrap();
15685        let mut frontend = FrontendState::new();
15686
15687        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15688        let mock_ctx = ExecutorContext::new_mock(None).await;
15689        let version = Version(0);
15690
15691        frontend.hack_set_program(&ctx, program).await.unwrap();
15692        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15693        let sketch_id = sketch_object.id;
15694
15695        let circle_ctor = CircleCtor {
15696            start: Point2d {
15697                x: Expr::Var(Number {
15698                    value: 5.0,
15699                    units: NumericSuffix::Mm,
15700                }),
15701                y: Expr::Var(Number {
15702                    value: 0.0,
15703                    units: NumericSuffix::Mm,
15704                }),
15705            },
15706            center: Point2d {
15707                x: Expr::Var(Number {
15708                    value: 0.0,
15709                    units: NumericSuffix::Mm,
15710                }),
15711                y: Expr::Var(Number {
15712                    value: 0.0,
15713                    units: NumericSuffix::Mm,
15714                }),
15715            },
15716            construction: None,
15717        };
15718        let segment = SegmentCtor::Circle(circle_ctor);
15719        let (src_delta, scene_delta) = frontend
15720            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15721            .await
15722            .unwrap();
15723        assert!(
15724            src_delta
15725                .text
15726                .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
15727            "Expected circle in source, got: {}",
15728            src_delta.text
15729        );
15730        assert!(!scene_delta.new_objects.is_empty());
15731
15732        ctx.close().await;
15733        mock_ctx.close().await;
15734    }
15735
15736    #[tokio::test(flavor = "multi_thread")]
15737    async fn test_extra_newlines_add_constraint() {
15738        // Extra newlines with a sketch containing two lines - add a coincident constraint.
15739        let initial_source = "@settings(defaultLengthUnit = mm)
15740
15741sketch001 = sketch(on = XY) {
15742  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
15743  line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
15744}
15745";
15746
15747        let program = Program::parse(initial_source).unwrap().0.unwrap();
15748        let mut frontend = FrontendState::new();
15749
15750        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15751        let mock_ctx = ExecutorContext::new_mock(None).await;
15752        let version = Version(0);
15753        let project_id = ProjectId(0);
15754        let file_id = FileId(0);
15755
15756        frontend.hack_set_program(&ctx, program).await.unwrap();
15757        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15758        let sketch_id = sketch_object.id;
15759        let sketch = expect_sketch(sketch_object);
15760
15761        // Extract segment data before edit_sketch borrows frontend mutably.
15762        let line_ids: Vec<ObjectId> = sketch
15763            .segments
15764            .iter()
15765            .copied()
15766            .filter(|seg_id| {
15767                matches!(
15768                    &frontend.scene_graph.objects[seg_id.0].kind,
15769                    ObjectKind::Segment {
15770                        segment: Segment::Line(_)
15771                    }
15772                )
15773            })
15774            .collect();
15775        assert_eq!(line_ids.len(), 2, "Expected two line segments");
15776
15777        let line1 = &frontend.scene_graph.objects[line_ids[0].0];
15778        let ObjectKind::Segment {
15779            segment: Segment::Line(line1_data),
15780        } = &line1.kind
15781        else {
15782            panic!("Expected line");
15783        };
15784        let line2 = &frontend.scene_graph.objects[line_ids[1].0];
15785        let ObjectKind::Segment {
15786            segment: Segment::Line(line2_data),
15787        } = &line2.kind
15788        else {
15789            panic!("Expected line");
15790        };
15791
15792        // Build constraint before entering sketch mode.
15793        let constraint = Constraint::Coincident(Coincident {
15794            segments: vec![line1_data.end.into(), line2_data.start.into()],
15795        });
15796
15797        // Enter sketch edit mode.
15798        frontend
15799            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15800            .await
15801            .unwrap();
15802        let (src_delta, _scene_delta) = frontend
15803            .add_constraint(&mock_ctx, version, sketch_id, constraint)
15804            .await
15805            .unwrap();
15806        assert!(
15807            src_delta.text.contains("coincident("),
15808            "Expected coincident constraint in source, got: {}",
15809            src_delta.text
15810        );
15811
15812        ctx.close().await;
15813        mock_ctx.close().await;
15814    }
15815
15816    #[tokio::test(flavor = "multi_thread")]
15817    async fn test_extra_newlines_add_line_then_edit_line() {
15818        // Extra newlines after @settings - add a line, then edit it.
15819        let initial_source = "@settings(defaultLengthUnit = mm)
15820
15821sketch001 = sketch(on = XY) {
15822}
15823";
15824
15825        let program = Program::parse(initial_source).unwrap().0.unwrap();
15826        let mut frontend = FrontendState::new();
15827
15828        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15829        let mock_ctx = ExecutorContext::new_mock(None).await;
15830        let version = Version(0);
15831
15832        frontend.hack_set_program(&ctx, program).await.unwrap();
15833        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15834        let sketch_id = sketch_object.id;
15835
15836        // Add a line.
15837        let line_ctor = LineCtor {
15838            start: Point2d {
15839                x: Expr::Number(Number {
15840                    value: 0.0,
15841                    units: NumericSuffix::Mm,
15842                }),
15843                y: Expr::Number(Number {
15844                    value: 0.0,
15845                    units: NumericSuffix::Mm,
15846                }),
15847            },
15848            end: Point2d {
15849                x: Expr::Number(Number {
15850                    value: 10.0,
15851                    units: NumericSuffix::Mm,
15852                }),
15853                y: Expr::Number(Number {
15854                    value: 10.0,
15855                    units: NumericSuffix::Mm,
15856                }),
15857            },
15858            construction: None,
15859        };
15860        let segment = SegmentCtor::Line(line_ctor);
15861        let (src_delta, scene_delta) = frontend
15862            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15863            .await
15864            .unwrap();
15865        assert!(
15866            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15867            "Expected line in source after add, got: {}",
15868            src_delta.text
15869        );
15870        // Line creates start point, end point, and line segment.
15871        let line_id = *scene_delta.new_objects.last().unwrap();
15872
15873        // Edit the line.
15874        let line_ctor = LineCtor {
15875            start: Point2d {
15876                x: Expr::Number(Number {
15877                    value: 1.0,
15878                    units: NumericSuffix::Mm,
15879                }),
15880                y: Expr::Number(Number {
15881                    value: 2.0,
15882                    units: NumericSuffix::Mm,
15883                }),
15884            },
15885            end: Point2d {
15886                x: Expr::Number(Number {
15887                    value: 13.0,
15888                    units: NumericSuffix::Mm,
15889                }),
15890                y: Expr::Number(Number {
15891                    value: 14.0,
15892                    units: NumericSuffix::Mm,
15893                }),
15894            },
15895            construction: None,
15896        };
15897        let segments = vec![ExistingSegmentCtor {
15898            id: line_id,
15899            ctor: SegmentCtor::Line(line_ctor),
15900        }];
15901        let (src_delta, scene_delta) = frontend
15902            .edit_segments(&mock_ctx, version, sketch_id, segments)
15903            .await
15904            .unwrap();
15905        assert!(
15906            src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
15907            "Expected edited line in source, got: {}",
15908            src_delta.text
15909        );
15910        assert_eq!(scene_delta.new_objects, vec![]);
15911
15912        ctx.close().await;
15913        mock_ctx.close().await;
15914    }
15915
15916    #[test]
15917    fn test_add_variable_declaration_uses_top_level_scope_after_sketch_block() {
15918        // A non-target sketch block appears before the target so that the
15919        // traversal enters and leaves it before reaching the target. The
15920        // generated name must come from the top-level scope, where foo1 is
15921        // taken, not the sketch's scope, where no foo names are taken. This
15922        // is a regression test: dfs_mut used to visit the sketch block twice,
15923        // pushing its scope twice but popping it once, leaving the sketch
15924        // scope on top of the defined-names stack for the rest of the
15925        // traversal.
15926        let code = "\
15927foo1 = 1
15928sk = sketch() {
15929  p = var 1.5
15930}
159317 + 8
15932";
15933        let mut ast = crate::parsing::top_level_parse(code).unwrap();
15934        let ast::BodyItem::ExpressionStatement(stmt) = &ast.body[2] else {
15935            panic!("expected an expression statement");
15936        };
15937        let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
15938        let (_, cmd_return) = mutate_ast_node_by_source_ref(
15939            &mut ast,
15940            &source_ref,
15941            AstMutateCommand::AddVariableDeclaration {
15942                prefix: "foo".to_owned(),
15943            },
15944        )
15945        .unwrap();
15946        let AstMutateCommandReturn::Name(name) = cmd_return else {
15947            panic!("expected a generated name");
15948        };
15949        assert_eq!(name, "foo2");
15950        let ast::BodyItem::VariableDeclaration(decl) = &ast.body[2] else {
15951            panic!("expected the expression statement to become a variable declaration");
15952        };
15953        assert_eq!(decl.name(), "foo2");
15954    }
15955
15956    /// Get the function body of the variable declaration at `ast.body[index]`.
15957    fn function_body_at(ast: &ast::Node<ast::Program>, index: usize) -> &ast::Node<ast::Program> {
15958        let ast::BodyItem::VariableDeclaration(decl) = &ast.body[index] else {
15959            panic!("expected a variable declaration");
15960        };
15961        let ast::Expr::FunctionExpression(func) = &decl.declaration.init else {
15962            panic!("expected a function expression");
15963        };
15964        &func.body
15965    }
15966
15967    /// Get the then-branch block of the if-expression initializing the
15968    /// variable declaration at `ast.body[index]`.
15969    fn then_block_at(ast: &ast::Node<ast::Program>, index: usize) -> &ast::Node<ast::Program> {
15970        let ast::BodyItem::VariableDeclaration(decl) = &ast.body[index] else {
15971            panic!("expected a variable declaration");
15972        };
15973        let ast::Expr::IfExpression(if_expr) = &decl.declaration.init else {
15974            panic!("expected an if expression");
15975        };
15976        &if_expr.then_val
15977    }
15978
15979    #[test]
15980    fn test_add_variable_declaration_in_function_body_uses_function_scope() {
15981        // The generated name must come from the function body's scope, where
15982        // thing1 is taken. Before dfs_mut visited function bodies as program
15983        // nodes, the top-level scope was used instead, generating thing1 and
15984        // colliding with the local.
15985        let code = "\
15986fn build() {
15987  thing1 = 1
15988  10 + 20
15989  return thing1
15990}
15991";
15992        let mut ast = crate::parsing::top_level_parse(code).unwrap();
15993        let ast::BodyItem::ExpressionStatement(stmt) = &function_body_at(&ast, 0).body[1] else {
15994            panic!("expected an expression statement");
15995        };
15996        let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
15997        let (_, cmd_return) = mutate_ast_node_by_source_ref(
15998            &mut ast,
15999            &source_ref,
16000            AstMutateCommand::AddVariableDeclaration {
16001                prefix: "thing".to_owned(),
16002            },
16003        )
16004        .unwrap();
16005        let AstMutateCommandReturn::Name(name) = cmd_return else {
16006            panic!("expected a generated name");
16007        };
16008        assert_eq!(name, "thing2");
16009        let body = &function_body_at(&ast, 0).body;
16010        assert_eq!(body.len(), 3);
16011        let ast::BodyItem::VariableDeclaration(decl) = &body[1] else {
16012            panic!("expected the expression statement to become a variable declaration");
16013        };
16014        assert_eq!(decl.name(), "thing2");
16015        // Siblings are untouched.
16016        let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16017            panic!("expected a variable declaration");
16018        };
16019        assert_eq!(first.name(), "thing1");
16020        assert!(matches!(&body[2], ast::BodyItem::ReturnStatement(_)));
16021    }
16022
16023    #[test]
16024    fn test_delete_node_in_function_body_preserves_leading_comment() {
16025        // Before the shared body traversal, MutateBodyItem::Delete was
16026        // silently dropped inside function bodies, so this reported success
16027        // without deleting anything.
16028        let code = "\
16029fn build() {
16030  a = 1
16031  // keep me
16032  b = 2
16033  return a
16034}
16035";
16036        let mut ast = crate::parsing::top_level_parse(code).unwrap();
16037        let ast::BodyItem::VariableDeclaration(b_decl) = &function_body_at(&ast, 0).body[1] else {
16038            panic!("expected a variable declaration");
16039        };
16040        assert_eq!(b_decl.name(), "b");
16041        let source_ref = SourceRef::new(SourceRange::from(&b_decl.declaration.init), None);
16042        mutate_ast_node_by_source_ref(&mut ast, &source_ref, AstMutateCommand::DeleteNode).unwrap();
16043        let body = &function_body_at(&ast, 0).body;
16044        assert_eq!(body.len(), 2, "expected b to be deleted");
16045        let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16046            panic!("expected a variable declaration");
16047        };
16048        assert_eq!(first.name(), "a");
16049        let ast::BodyItem::ReturnStatement(_) = &body[1] else {
16050            panic!("expected the return statement to remain");
16051        };
16052        assert!(
16053            body[1].get_comments().iter().any(|c| c.contains("keep me")),
16054            "expected the deleted item's leading comment to migrate to the next item, got: {:?}",
16055            body[1].get_comments()
16056        );
16057    }
16058
16059    #[test]
16060    fn test_add_variable_declaration_in_function_body_ignores_parameters() {
16061        // Locals in the function body are avoided: thing1 is taken, so the
16062        // generated name is thing2. But find_defined_names only sees the
16063        // block's body items, not the function's parameters, so the generated
16064        // name collides with the thing2 parameter. This pins the current
16065        // behavior.
16066        // TODO: Should function parameters be included in the scope used for
16067        // name generation?
16068        let code = "\
16069fn build(thing2) {
16070  thing1 = 1
16071  10 + 20
16072  return thing1 + thing2
16073}
16074";
16075        let mut ast = crate::parsing::top_level_parse(code).unwrap();
16076        let ast::BodyItem::ExpressionStatement(stmt) = &function_body_at(&ast, 0).body[1] else {
16077            panic!("expected an expression statement");
16078        };
16079        let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
16080        let (_, cmd_return) = mutate_ast_node_by_source_ref(
16081            &mut ast,
16082            &source_ref,
16083            AstMutateCommand::AddVariableDeclaration {
16084                prefix: "thing".to_owned(),
16085            },
16086        )
16087        .unwrap();
16088        let AstMutateCommandReturn::Name(name) = cmd_return else {
16089            panic!("expected a generated name");
16090        };
16091        assert_eq!(name, "thing2", "locals are avoided, but parameters are not");
16092    }
16093
16094    #[test]
16095    fn test_add_variable_declaration_in_if_branch_uses_branch_scope() {
16096        let code = "\
16097x = 1
16098y = if x > 0 {
16099  q1 = 1
16100  foo(q1)
16101  q1
16102} else {
16103  2
16104}
16105";
16106        let mut ast = crate::parsing::top_level_parse(code).unwrap();
16107        let ast::BodyItem::ExpressionStatement(stmt) = &then_block_at(&ast, 1).body[1] else {
16108            panic!("expected an expression statement");
16109        };
16110        let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
16111        let (_, cmd_return) = mutate_ast_node_by_source_ref(
16112            &mut ast,
16113            &source_ref,
16114            AstMutateCommand::AddVariableDeclaration { prefix: "q".to_owned() },
16115        )
16116        .unwrap();
16117        let AstMutateCommandReturn::Name(name) = cmd_return else {
16118            panic!("expected a generated name");
16119        };
16120        assert_eq!(name, "q2");
16121        let body = &then_block_at(&ast, 1).body;
16122        assert_eq!(body.len(), 3);
16123        let ast::BodyItem::VariableDeclaration(decl) = &body[1] else {
16124            panic!("expected the expression statement to become a variable declaration");
16125        };
16126        assert_eq!(decl.name(), "q2");
16127        // Siblings are untouched.
16128        let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16129            panic!("expected a variable declaration");
16130        };
16131        assert_eq!(first.name(), "q1");
16132        assert!(matches!(&body[2], ast::BodyItem::ExpressionStatement(_)));
16133    }
16134
16135    #[test]
16136    fn test_delete_node_in_if_branch_preserves_leading_comment() {
16137        // Before the shared body traversal, MutateBodyItem::Delete was
16138        // silently dropped inside if-expression branch blocks.
16139        let code = "\
16140y = if true {
16141  a = 1
16142  // keep me
16143  b = 2
16144  a + b
16145} else {
16146  2
16147}
16148";
16149        let mut ast = crate::parsing::top_level_parse(code).unwrap();
16150        let ast::BodyItem::VariableDeclaration(b_decl) = &then_block_at(&ast, 0).body[1] else {
16151            panic!("expected a variable declaration");
16152        };
16153        assert_eq!(b_decl.name(), "b");
16154        let source_ref = SourceRef::new(SourceRange::from(&b_decl.declaration.init), None);
16155        mutate_ast_node_by_source_ref(&mut ast, &source_ref, AstMutateCommand::DeleteNode).unwrap();
16156        let body = &then_block_at(&ast, 0).body;
16157        assert_eq!(body.len(), 2, "expected b to be deleted");
16158        let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16159            panic!("expected a variable declaration");
16160        };
16161        assert_eq!(first.name(), "a");
16162        let ast::BodyItem::ExpressionStatement(_) = &body[1] else {
16163            panic!("expected the tail expression to remain");
16164        };
16165        assert!(
16166            body[1].get_comments().iter().any(|c| c.contains("keep me")),
16167            "expected the deleted item's leading comment to migrate to the next item, got: {:?}",
16168            body[1].get_comments()
16169        );
16170    }
16171}