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            default_planes,
2057            ..
2058        } = err;
2059
2060        non_fatal.push(CompilationIssue::fatal(issue_source_range(&error), error.get_message()));
2061
2062        Ok(ExecOutcome {
2063            variables,
2064            filenames,
2065            operations,
2066            artifact_graph,
2067            scene_objects,
2068            source_range_to_object,
2069            var_solutions,
2070            refactor_metadata,
2071            issues: non_fatal,
2072            default_planes,
2073        })
2074    }
2075
2076    async fn add_point(
2077        &mut self,
2078        ctx: &ExecutorContext,
2079        sketch: ObjectId,
2080        ctor: PointCtor,
2081    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2082        // Create updated KCL source from args.
2083        let at_ast = to_ast_point2d(&ctor.position)
2084            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2085        let point_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2086            callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
2087            unlabeled: None,
2088            arguments: vec![ast::LabeledArg {
2089                label: Some(ast::Identifier::new(POINT_AT_PARAM)),
2090                arg: at_ast,
2091            }],
2092            digest: None,
2093            non_code_meta: Default::default(),
2094        })));
2095
2096        // Look up existing sketch.
2097        let sketch_id = sketch;
2098        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2099            #[cfg(target_arch = "wasm32")]
2100            web_sys::console::error_1(
2101                &format!(
2102                    "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
2103                    self.scene_graph.objects
2104                )
2105                .into(),
2106            );
2107            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2108        })?;
2109        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2110            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2111                "Object is not a sketch, it is {}",
2112                sketch_object.kind.human_friendly_kind_with_article(),
2113            ))));
2114        };
2115        // Add the point to the AST of the sketch block.
2116        let mut new_ast = self.program.ast.clone();
2117        let (sketch_block_ref, _) = self
2118            .mutate_ast(
2119                &mut new_ast,
2120                sketch_id,
2121                AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
2122            )
2123            .map_err(KclErrorWithOutputs::no_outputs)?;
2124        // Convert to string source to create real source ranges.
2125        let new_source = source_from_ast(&new_ast);
2126        // Parse the new KCL source.
2127        let new_program = parse_frontend_mutation_source(
2128            &new_source,
2129            "Error parsing KCL source after adding point",
2130            "No AST produced after adding point",
2131        )?;
2132
2133        let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2134            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2135                "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
2136            )))
2137        })?;
2138
2139        // Make sure to only set this if there are no errors.
2140        self.program = new_program.clone();
2141
2142        // Truncate after the sketch block for mock execution.
2143        let mut truncated_program = new_program;
2144        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2145            .map_err(KclErrorWithOutputs::no_outputs)?;
2146
2147        // Execute.
2148        let outcome = ctx
2149            .run_mock(
2150                &truncated_program,
2151                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2152            )
2153            .await?;
2154
2155        let new_object_ids = {
2156            let make_err =
2157                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2158            let segment_id = outcome
2159                .source_range_to_object
2160                .get(&point_node_ref.range)
2161                .copied()
2162                .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
2163            let segment_object = outcome
2164                .scene_objects
2165                .get(segment_id.0)
2166                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2167            let ObjectKind::Segment { segment } = &segment_object.kind else {
2168                return Err(make_err(format!(
2169                    "Object is not a segment, it is {}",
2170                    segment_object.kind.human_friendly_kind_with_article()
2171                )));
2172            };
2173            let Segment::Point(_) = segment else {
2174                return Err(make_err(format!(
2175                    "Segment is not a point, it is {}",
2176                    segment.human_friendly_kind_with_article()
2177                )));
2178            };
2179            vec![segment_id]
2180        };
2181        let src_delta = SourceDelta { text: new_source };
2182        // Uses .no_freedom_analysis() so freedom_analysis: false
2183        let outcome = self.update_state_after_exec(outcome, false);
2184        let scene_graph_delta = SceneGraphDelta {
2185            new_graph: self.scene_graph_for_ui(),
2186            invalidates_ids: false,
2187            new_objects: new_object_ids,
2188            exec_outcome: outcome,
2189        };
2190        Ok((src_delta, scene_graph_delta))
2191    }
2192
2193    async fn add_line(
2194        &mut self,
2195        ctx: &ExecutorContext,
2196        sketch: ObjectId,
2197        ctor: LineCtor,
2198    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2199        // Create updated KCL source from args.
2200        let start_ast = to_ast_point2d(&ctor.start)
2201            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2202        let end_ast = to_ast_point2d(&ctor.end)
2203            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2204        let mut arguments = vec![
2205            ast::LabeledArg {
2206                label: Some(ast::Identifier::new(LINE_START_PARAM)),
2207                arg: start_ast,
2208            },
2209            ast::LabeledArg {
2210                label: Some(ast::Identifier::new(LINE_END_PARAM)),
2211                arg: end_ast,
2212            },
2213        ];
2214        // Add construction kwarg if construction is Some(true)
2215        if ctor.construction == Some(true) {
2216            arguments.push(ast::LabeledArg {
2217                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2218                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2219                    value: ast::LiteralValue::Bool(true),
2220                    raw: "true".to_string(),
2221                    digest: None,
2222                }))),
2223            });
2224        }
2225        let line_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2226            callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
2227            unlabeled: None,
2228            arguments,
2229            digest: None,
2230            non_code_meta: Default::default(),
2231        })));
2232
2233        // Look up existing sketch.
2234        let sketch_id = sketch;
2235        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2236            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2237        })?;
2238        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2239            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2240                "Object is not a sketch, it is {}",
2241                sketch_object.kind.human_friendly_kind_with_article(),
2242            ))));
2243        };
2244        // Add the line to the AST of the sketch block.
2245        let mut new_ast = self.program.ast.clone();
2246        let (sketch_block_ref, _) = self
2247            .mutate_ast(
2248                &mut new_ast,
2249                sketch_id,
2250                AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
2251            )
2252            .map_err(KclErrorWithOutputs::no_outputs)?;
2253        // Convert to string source to create real source ranges.
2254        let new_source = source_from_ast(&new_ast);
2255        // Parse the new KCL source.
2256        let new_program = parse_frontend_mutation_source(
2257            &new_source,
2258            "Error parsing KCL source after adding line",
2259            "No AST produced after adding line",
2260        )?;
2261
2262        let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2263            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2264                "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
2265            )))
2266        })?;
2267
2268        // Make sure to only set this if there are no errors.
2269        self.program = new_program.clone();
2270
2271        // Truncate after the sketch block for mock execution.
2272        let mut truncated_program = new_program;
2273        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2274            .map_err(KclErrorWithOutputs::no_outputs)?;
2275
2276        // Execute.
2277        let outcome = ctx
2278            .run_mock(
2279                &truncated_program,
2280                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2281            )
2282            .await?;
2283
2284        let new_object_ids = {
2285            let make_err =
2286                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2287            let segment_id = outcome
2288                .source_range_to_object
2289                .get(&line_node_ref.range)
2290                .copied()
2291                .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
2292            let segment_object = outcome
2293                .scene_object_by_id(segment_id)
2294                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2295            let ObjectKind::Segment { segment } = &segment_object.kind else {
2296                return Err(make_err(format!(
2297                    "Object is not a segment, it is {}",
2298                    segment_object.kind.human_friendly_kind_with_article()
2299                )));
2300            };
2301            let Segment::Line(line) = segment else {
2302                return Err(make_err(format!(
2303                    "Segment is not a line, it is {}",
2304                    segment.human_friendly_kind_with_article()
2305                )));
2306            };
2307            vec![line.start, line.end, segment_id]
2308        };
2309        let src_delta = SourceDelta { text: new_source };
2310        // Uses .no_freedom_analysis() so freedom_analysis: false
2311        let outcome = self.update_state_after_exec(outcome, false);
2312        let scene_graph_delta = SceneGraphDelta {
2313            new_graph: self.scene_graph_for_ui(),
2314            invalidates_ids: false,
2315            new_objects: new_object_ids,
2316            exec_outcome: outcome,
2317        };
2318        Ok((src_delta, scene_graph_delta))
2319    }
2320
2321    async fn add_arc(
2322        &mut self,
2323        ctx: &ExecutorContext,
2324        sketch: ObjectId,
2325        ctor: ArcCtor,
2326    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2327        // Create updated KCL source from args.
2328        let start_ast = to_ast_point2d(&ctor.start)
2329            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2330        let end_ast = to_ast_point2d(&ctor.end)
2331            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2332        let center_ast = to_ast_point2d(&ctor.center)
2333            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2334        let mut arguments = vec![
2335            ast::LabeledArg {
2336                label: Some(ast::Identifier::new(ARC_START_PARAM)),
2337                arg: start_ast,
2338            },
2339            ast::LabeledArg {
2340                label: Some(ast::Identifier::new(ARC_END_PARAM)),
2341                arg: end_ast,
2342            },
2343            ast::LabeledArg {
2344                label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
2345                arg: center_ast,
2346            },
2347        ];
2348        // Add direction kwarg if it's clockwise, since counterclockwise is the
2349        // default.
2350        if ctor.direction == Some(ArcDirection::Cw) {
2351            arguments.push(ast::LabeledArg {
2352                label: Some(ast::Identifier::new(ARC_DIRECTION_PARAM)),
2353                arg: ast::Expr::Name(BoxNode::new(ast::Name::new(ARC_DIRECTION_CW_NAME))),
2354            });
2355        }
2356        // Add construction kwarg if construction is Some(true)
2357        if ctor.construction == Some(true) {
2358            arguments.push(ast::LabeledArg {
2359                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2360                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2361                    value: ast::LiteralValue::Bool(true),
2362                    raw: "true".to_string(),
2363                    digest: None,
2364                }))),
2365            });
2366        }
2367        let arc_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2368            callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
2369            unlabeled: None,
2370            arguments,
2371            digest: None,
2372            non_code_meta: Default::default(),
2373        })));
2374
2375        // Look up existing sketch.
2376        let sketch_id = sketch;
2377        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2378            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2379        })?;
2380        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2381            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2382                "Object is not a sketch, it is {}",
2383                sketch_object.kind.human_friendly_kind_with_article(),
2384            ))));
2385        };
2386        // Add the arc to the AST of the sketch block.
2387        let mut new_ast = self.program.ast.clone();
2388        let (sketch_block_ref, _) = self
2389            .mutate_ast(
2390                &mut new_ast,
2391                sketch_id,
2392                AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
2393            )
2394            .map_err(KclErrorWithOutputs::no_outputs)?;
2395        // Convert to string source to create real source ranges.
2396        let new_source = source_from_ast(&new_ast);
2397        // Parse the new KCL source.
2398        let new_program = parse_frontend_mutation_source(
2399            &new_source,
2400            "Error parsing KCL source after adding arc",
2401            "No AST produced after adding arc",
2402        )?;
2403
2404        let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2405            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2406                "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
2407            )))
2408        })?;
2409
2410        // Make sure to only set this if there are no errors.
2411        self.program = new_program.clone();
2412
2413        // Truncate after the sketch block for mock execution.
2414        let mut truncated_program = new_program;
2415        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2416            .map_err(KclErrorWithOutputs::no_outputs)?;
2417
2418        // Execute.
2419        let outcome = ctx
2420            .run_mock(
2421                &truncated_program,
2422                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2423            )
2424            .await?;
2425
2426        let new_object_ids = {
2427            let make_err =
2428                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2429            let segment_id = outcome
2430                .source_range_to_object
2431                .get(&arc_node_ref.range)
2432                .copied()
2433                .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
2434            let segment_object = outcome
2435                .scene_objects
2436                .get(segment_id.0)
2437                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2438            let ObjectKind::Segment { segment } = &segment_object.kind else {
2439                return Err(make_err(format!(
2440                    "Object is not a segment, it is {}",
2441                    segment_object.kind.human_friendly_kind_with_article()
2442                )));
2443            };
2444            let Segment::Arc(arc) = segment else {
2445                return Err(make_err(format!(
2446                    "Segment is not an arc, it is {}",
2447                    segment.human_friendly_kind_with_article()
2448                )));
2449            };
2450            vec![arc.start, arc.end, arc.center, segment_id]
2451        };
2452        let src_delta = SourceDelta { text: new_source };
2453        // Uses .no_freedom_analysis() so freedom_analysis: false
2454        let outcome = self.update_state_after_exec(outcome, false);
2455        let scene_graph_delta = SceneGraphDelta {
2456            new_graph: self.scene_graph_for_ui(),
2457            invalidates_ids: false,
2458            new_objects: new_object_ids,
2459            exec_outcome: outcome,
2460        };
2461        Ok((src_delta, scene_graph_delta))
2462    }
2463
2464    async fn add_circle(
2465        &mut self,
2466        ctx: &ExecutorContext,
2467        sketch: ObjectId,
2468        ctor: CircleCtor,
2469    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2470        // Create updated KCL source from args.
2471        let start_ast = to_ast_point2d(&ctor.start)
2472            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2473        let center_ast = to_ast_point2d(&ctor.center)
2474            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2475        let mut arguments = vec![
2476            ast::LabeledArg {
2477                label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2478                arg: start_ast,
2479            },
2480            ast::LabeledArg {
2481                label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2482                arg: center_ast,
2483            },
2484        ];
2485        // Add construction kwarg if construction is Some(true)
2486        if ctor.construction == Some(true) {
2487            arguments.push(ast::LabeledArg {
2488                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2489                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2490                    value: ast::LiteralValue::Bool(true),
2491                    raw: "true".to_string(),
2492                    digest: None,
2493                }))),
2494            });
2495        }
2496        let circle_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2497            callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2498            unlabeled: None,
2499            arguments,
2500            digest: None,
2501            non_code_meta: Default::default(),
2502        })));
2503
2504        // Look up existing sketch.
2505        let sketch_id = sketch;
2506        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2507            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2508        })?;
2509        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2510            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2511                "Object is not a sketch, it is {}",
2512                sketch_object.kind.human_friendly_kind_with_article(),
2513            ))));
2514        };
2515        // Add the circle to the AST of the sketch block.
2516        let mut new_ast = self.program.ast.clone();
2517        let (sketch_block_ref, _) = self
2518            .mutate_ast(
2519                &mut new_ast,
2520                sketch_id,
2521                AstMutateCommand::AddSketchBlockVarDecl {
2522                    prefix: CIRCLE_VARIABLE.to_owned(),
2523                    expr: circle_ast,
2524                },
2525            )
2526            .map_err(KclErrorWithOutputs::no_outputs)?;
2527        // Convert to string source to create real source ranges.
2528        let new_source = source_from_ast(&new_ast);
2529        // Parse the new KCL source.
2530        let new_program = parse_frontend_mutation_source(
2531            &new_source,
2532            "Error parsing KCL source after adding circle",
2533            "No AST produced after adding circle",
2534        )?;
2535
2536        let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2537            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2538                "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2539            )))
2540        })?;
2541
2542        // Make sure to only set this if there are no errors.
2543        self.program = new_program.clone();
2544
2545        // Truncate after the sketch block for mock execution.
2546        let mut truncated_program = new_program;
2547        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2548            .map_err(KclErrorWithOutputs::no_outputs)?;
2549
2550        // Execute.
2551        let outcome = ctx
2552            .run_mock(
2553                &truncated_program,
2554                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2555            )
2556            .await?;
2557
2558        let new_object_ids = {
2559            let make_err =
2560                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2561            let segment_id = outcome
2562                .source_range_to_object
2563                .get(&circle_node_ref.range)
2564                .copied()
2565                .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2566            let segment_object = outcome
2567                .scene_objects
2568                .get(segment_id.0)
2569                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2570            let ObjectKind::Segment { segment } = &segment_object.kind else {
2571                return Err(make_err(format!(
2572                    "Object is not a segment, it is {}",
2573                    segment_object.kind.human_friendly_kind_with_article()
2574                )));
2575            };
2576            let Segment::Circle(circle) = segment else {
2577                return Err(make_err(format!(
2578                    "Segment is not a circle, it is {}",
2579                    segment.human_friendly_kind_with_article()
2580                )));
2581            };
2582            vec![circle.start, circle.center, segment_id]
2583        };
2584        let src_delta = SourceDelta { text: new_source };
2585        // Uses .no_freedom_analysis() so freedom_analysis: false
2586        let outcome = self.update_state_after_exec(outcome, false);
2587        let scene_graph_delta = SceneGraphDelta {
2588            new_graph: self.scene_graph_for_ui(),
2589            invalidates_ids: false,
2590            new_objects: new_object_ids,
2591            exec_outcome: outcome,
2592        };
2593        Ok((src_delta, scene_graph_delta))
2594    }
2595
2596    async fn add_control_point_spline(
2597        &mut self,
2598        ctx: &ExecutorContext,
2599        sketch: ObjectId,
2600        ctor: ControlPointSplineCtor,
2601    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2602        let new_program = ensure_control_point_spline_experimental_features(&self.program)
2603            .map_err(KclErrorWithOutputs::no_outputs)?;
2604
2605        let points_ast = to_ast_point2d_array(&ctor.points)
2606            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2607        let mut arguments = vec![ast::LabeledArg {
2608            label: Some(ast::Identifier::new(CONTROL_POINT_SPLINE_POINTS_PARAM)),
2609            arg: points_ast,
2610        }];
2611        if ctor.construction == Some(true) {
2612            arguments.push(ast::LabeledArg {
2613                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2614                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2615                    value: ast::LiteralValue::Bool(true),
2616                    raw: "true".to_string(),
2617                    digest: None,
2618                }))),
2619            });
2620        }
2621        let spline_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2622            callee: ast::Node::no_src(ast_sketch2_name(CONTROL_POINT_SPLINE_FN)),
2623            unlabeled: None,
2624            arguments,
2625            digest: None,
2626            non_code_meta: Default::default(),
2627        })));
2628
2629        let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
2630            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2631        })?;
2632        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2633            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2634                "Object is not a sketch, it is {}",
2635                sketch_object.kind.human_friendly_kind_with_article(),
2636            ))));
2637        };
2638
2639        let mut new_ast = new_program.ast.clone();
2640        let (sketch_block_ref, _) = self
2641            .mutate_ast(
2642                &mut new_ast,
2643                sketch,
2644                AstMutateCommand::AddSketchBlockExprStmt { expr: spline_ast },
2645            )
2646            .map_err(KclErrorWithOutputs::no_outputs)?;
2647        let new_source = source_from_ast(&new_ast);
2648        let new_program = parse_frontend_mutation_source(
2649            &new_source,
2650            "Error parsing KCL source after adding controlPointSpline",
2651            "No AST produced after adding controlPointSpline",
2652        )?;
2653
2654        let spline_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2655            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2656                "Source range of controlPointSpline not found in sketch block: {sketch_block_ref:?}; {err:?}"
2657            )))
2658        })?;
2659
2660        self.program = new_program.clone();
2661
2662        let mut truncated_program = new_program;
2663        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2664            .map_err(KclErrorWithOutputs::no_outputs)?;
2665
2666        let outcome = ctx
2667            .run_mock(
2668                &truncated_program,
2669                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2670            )
2671            .await?;
2672
2673        let new_object_ids = {
2674            let make_err =
2675                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2676            let segment_id = outcome
2677                .source_range_to_object
2678                .get(&spline_node_ref.range)
2679                .copied()
2680                .ok_or_else(|| {
2681                    make_err(format!(
2682                        "Source range of controlPointSpline not found: {spline_node_ref:?}"
2683                    ))
2684                })?;
2685            let segment_object = outcome
2686                .scene_objects
2687                .get(segment_id.0)
2688                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2689            let ObjectKind::Segment { segment } = &segment_object.kind else {
2690                return Err(make_err(format!(
2691                    "Object is not a segment, it is {}",
2692                    segment_object.kind.human_friendly_kind_with_article()
2693                )));
2694            };
2695            let Segment::ControlPointSpline(spline) = segment else {
2696                return Err(make_err(format!(
2697                    "Segment is not a control point spline, it is {}",
2698                    segment.human_friendly_kind_with_article()
2699                )));
2700            };
2701
2702            let mut ids = outcome
2703                .scene_objects
2704                .iter()
2705                .filter_map(|obj| match &obj.kind {
2706                    ObjectKind::Segment {
2707                        segment: Segment::Line(line),
2708                    } if line.owner == Some(segment_id) => Some(obj.id),
2709                    _ => None,
2710                })
2711                .collect::<Vec<_>>();
2712            ids.extend(spline.controls.clone());
2713            ids.push(segment_id);
2714            ids
2715        };
2716        let src_delta = SourceDelta { text: new_source };
2717        let outcome = self.update_state_after_exec(outcome, false);
2718        let scene_graph_delta = SceneGraphDelta {
2719            new_graph: self.scene_graph_for_ui(),
2720            invalidates_ids: false,
2721            new_objects: new_object_ids,
2722            exec_outcome: outcome,
2723        };
2724        Ok((src_delta, scene_graph_delta))
2725    }
2726
2727    fn edit_point(
2728        &mut self,
2729        new_ast: &mut ast::Node<ast::Program>,
2730        sketch: ObjectId,
2731        point: ObjectId,
2732        ctor: PointCtor,
2733    ) -> Result<(), KclError> {
2734        // Create updated KCL source from args.
2735        let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2736
2737        // Look up existing sketch.
2738        let sketch_id = sketch;
2739        let sketch_object = self
2740            .scene_graph
2741            .objects
2742            .get(sketch_id.0)
2743            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2744        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2745            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2746        };
2747        sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2748            KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2749        })?;
2750        // Look up existing point.
2751        let point_id = point;
2752        let point_object = self
2753            .scene_graph
2754            .objects
2755            .get(point_id.0)
2756            .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2757        let ObjectKind::Segment {
2758            segment: Segment::Point(point),
2759        } = &point_object.kind
2760        else {
2761            return Err(KclError::refactor(format!(
2762                "Object is not a point segment: {point_object:?}"
2763            )));
2764        };
2765
2766        // If the point is part of a line or arc, edit the line/arc instead.
2767        if let Some(owner_id) = point.owner {
2768            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2769                KclError::refactor(format!(
2770                    "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2771                ))
2772            })?;
2773            let ObjectKind::Segment { segment } = &owner_object.kind else {
2774                return Err(KclError::refactor(format!(
2775                    "Internal: Owner of point is not a segment, but found {}",
2776                    owner_object.kind.human_friendly_kind_with_article()
2777                )));
2778            };
2779
2780            // Handle Line owner
2781            if let Segment::Line(line) = segment {
2782                let SegmentCtor::Line(line_ctor) = &line.ctor else {
2783                    return Err(KclError::refactor(format!(
2784                        "Internal: Owner of point does not have line ctor, but found {}",
2785                        line.ctor.human_friendly_kind_with_article()
2786                    )));
2787                };
2788                let mut line_ctor = line_ctor.clone();
2789                // Which end of the line is this point?
2790                if line.start == point_id {
2791                    line_ctor.start = ctor.position;
2792                } else if line.end == point_id {
2793                    line_ctor.end = ctor.position;
2794                } else {
2795                    return Err(KclError::refactor(format!(
2796                        "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2797                    )));
2798                }
2799                return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2800            }
2801
2802            // Handle Arc owner
2803            if let Segment::Arc(arc) = segment {
2804                let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2805                    return Err(KclError::refactor(format!(
2806                        "Internal: Owner of point does not have arc ctor, but found {}",
2807                        arc.ctor.human_friendly_kind_with_article()
2808                    )));
2809                };
2810                let mut arc_ctor = arc_ctor.clone();
2811                // Which point of the arc is this? (center, start, or end)
2812                if arc.center == point_id {
2813                    arc_ctor.center = ctor.position;
2814                } else if arc.start == point_id {
2815                    arc_ctor.start = ctor.position;
2816                } else if arc.end == point_id {
2817                    arc_ctor.end = ctor.position;
2818                } else {
2819                    return Err(KclError::refactor(format!(
2820                        "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2821                    )));
2822                }
2823                return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2824            }
2825
2826            // Handle Circle owner
2827            if let Segment::Circle(circle) = segment {
2828                let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2829                    return Err(KclError::refactor(format!(
2830                        "Internal: Owner of point does not have circle ctor, but found {}",
2831                        circle.ctor.human_friendly_kind_with_article()
2832                    )));
2833                };
2834                let mut circle_ctor = circle_ctor.clone();
2835                if circle.center == point_id {
2836                    circle_ctor.center = ctor.position;
2837                } else if circle.start == point_id {
2838                    circle_ctor.start = ctor.position;
2839                } else {
2840                    return Err(KclError::refactor(format!(
2841                        "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2842                    )));
2843                }
2844                return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2845            }
2846
2847            if let Segment::ControlPointSpline(spline) = segment {
2848                let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor else {
2849                    return Err(KclError::refactor(format!(
2850                        "Internal: Owner of point does not have controlPointSpline ctor, but found {}",
2851                        spline.ctor.human_friendly_kind_with_article()
2852                    )));
2853                };
2854                let mut spline_ctor = spline_ctor.clone();
2855                let Some(control_index) = spline.controls.iter().position(|id| *id == point_id) else {
2856                    return Err(KclError::refactor(format!(
2857                        "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
2858                    )));
2859                };
2860                spline_ctor.points[control_index] = ctor.position;
2861                return self.edit_control_point_spline(new_ast, sketch_id, owner_id, spline_ctor);
2862            }
2863
2864            // If owner is neither Line, Arc, nor Circle, allow editing the point directly
2865            // (fall through to the point editing logic below)
2866        }
2867
2868        // Modify the point AST.
2869        self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2870        Ok(())
2871    }
2872
2873    fn edit_line(
2874        &mut self,
2875        new_ast: &mut ast::Node<ast::Program>,
2876        sketch: ObjectId,
2877        line: ObjectId,
2878        ctor: LineCtor,
2879    ) -> Result<(), KclError> {
2880        // Create updated KCL source from args.
2881        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2882        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2883
2884        // Look up existing sketch.
2885        let sketch_id = sketch;
2886        let sketch_object = self
2887            .scene_graph
2888            .objects
2889            .get(sketch_id.0)
2890            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2891        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2892            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2893        };
2894        sketch
2895            .segments
2896            .iter()
2897            .find(|o| **o == line)
2898            .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2899        // Look up existing line.
2900        let line_id = line;
2901        let line_object = self
2902            .scene_graph
2903            .objects
2904            .get(line_id.0)
2905            .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2906        let ObjectKind::Segment { .. } = &line_object.kind else {
2907            let kind = line_object.kind.human_friendly_kind_with_article();
2908            return Err(KclError::refactor(format!(
2909                "This constraint only works on Segments, but you selected {kind}"
2910            )));
2911        };
2912
2913        // Modify the line AST.
2914        self.mutate_ast(
2915            new_ast,
2916            line_id,
2917            AstMutateCommand::EditLine {
2918                start: new_start_ast,
2919                end: new_end_ast,
2920                construction: ctor.construction,
2921            },
2922        )?;
2923        Ok(())
2924    }
2925
2926    fn edit_arc(
2927        &mut self,
2928        new_ast: &mut ast::Node<ast::Program>,
2929        sketch: ObjectId,
2930        arc: ObjectId,
2931        ctor: ArcCtor,
2932    ) -> Result<(), KclError> {
2933        // Create updated KCL source from args.
2934        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2935        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2936        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2937
2938        // Look up existing sketch.
2939        let sketch_id = sketch;
2940        let sketch_object = self
2941            .scene_graph
2942            .objects
2943            .get(sketch_id.0)
2944            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2945        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2946            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2947        };
2948        sketch
2949            .segments
2950            .iter()
2951            .find(|o| **o == arc)
2952            .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2953        // Look up existing arc.
2954        let arc_id = arc;
2955        let arc_object = self
2956            .scene_graph
2957            .objects
2958            .get(arc_id.0)
2959            .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2960        let ObjectKind::Segment { .. } = &arc_object.kind else {
2961            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2962        };
2963
2964        // Modify the arc AST.
2965        self.mutate_ast(
2966            new_ast,
2967            arc_id,
2968            AstMutateCommand::EditArc {
2969                start: new_start_ast,
2970                end: new_end_ast,
2971                center: new_center_ast,
2972                direction: ctor.direction,
2973                construction: ctor.construction,
2974            },
2975        )?;
2976        Ok(())
2977    }
2978
2979    fn edit_circle(
2980        &mut self,
2981        new_ast: &mut ast::Node<ast::Program>,
2982        sketch: ObjectId,
2983        circle: ObjectId,
2984        ctor: CircleCtor,
2985    ) -> Result<(), KclError> {
2986        // Create updated KCL source from args.
2987        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2988        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2989
2990        // Look up existing sketch.
2991        let sketch_id = sketch;
2992        let sketch_object = self
2993            .scene_graph
2994            .objects
2995            .get(sketch_id.0)
2996            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2997        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2998            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2999        };
3000        sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
3001            KclError::refactor(format!(
3002                "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
3003            ))
3004        })?;
3005        // Look up existing circle.
3006        let circle_id = circle;
3007        let circle_object = self
3008            .scene_graph
3009            .objects
3010            .get(circle_id.0)
3011            .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
3012        let ObjectKind::Segment { .. } = &circle_object.kind else {
3013            return Err(KclError::refactor(format!(
3014                "Object is not a segment: {circle_object:?}"
3015            )));
3016        };
3017
3018        // Modify the circle AST.
3019        self.mutate_ast(
3020            new_ast,
3021            circle_id,
3022            AstMutateCommand::EditCircle {
3023                start: new_start_ast,
3024                center: new_center_ast,
3025                construction: ctor.construction,
3026            },
3027        )?;
3028        Ok(())
3029    }
3030
3031    fn edit_control_point_spline(
3032        &mut self,
3033        new_ast: &mut ast::Node<ast::Program>,
3034        sketch: ObjectId,
3035        spline: ObjectId,
3036        ctor: ControlPointSplineCtor,
3037    ) -> Result<(), KclError> {
3038        let points_ast = to_ast_point2d_array(&ctor.points).map_err(|err| KclError::refactor(err.to_string()))?;
3039
3040        let sketch_object = self
3041            .scene_graph
3042            .objects
3043            .get(sketch.0)
3044            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3045        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3046            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3047        };
3048        sketch.segments.iter().find(|o| **o == spline).ok_or_else(|| {
3049            KclError::refactor(format!(
3050                "Control point spline not found in sketch: spline={spline:?}, sketch={sketch:?}"
3051            ))
3052        })?;
3053
3054        let spline_object =
3055            self.scene_graph.objects.get(spline.0).ok_or_else(|| {
3056                KclError::refactor(format!("Control point spline not found in scene graph: {spline:?}"))
3057            })?;
3058        let ObjectKind::Segment { .. } = &spline_object.kind else {
3059            return Err(KclError::refactor(format!(
3060                "Object is not a segment: {spline_object:?}"
3061            )));
3062        };
3063
3064        self.mutate_ast(
3065            new_ast,
3066            spline,
3067            AstMutateCommand::EditControlPointSpline {
3068                points: points_ast,
3069                construction: ctor.construction,
3070            },
3071        )?;
3072        Ok(())
3073    }
3074
3075    fn delete_segment(
3076        &mut self,
3077        new_ast: &mut ast::Node<ast::Program>,
3078        sketch: ObjectId,
3079        segment_id: ObjectId,
3080    ) -> Result<(), KclError> {
3081        // Look up existing sketch.
3082        let sketch_id = sketch;
3083        let sketch_object = self
3084            .scene_graph
3085            .objects
3086            .get(sketch_id.0)
3087            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3088        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3089            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3090        };
3091        sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
3092            KclError::refactor(format!(
3093                "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
3094            ))
3095        })?;
3096        // Look up existing segment.
3097        let segment_object =
3098            self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
3099                KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
3100            })?;
3101        let ObjectKind::Segment { .. } = &segment_object.kind else {
3102            return Err(KclError::refactor(format!(
3103                "Object is not a segment, it is {}",
3104                segment_object.kind.human_friendly_kind_with_article()
3105            )));
3106        };
3107
3108        // Modify the AST to remove the segment.
3109        self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
3110        Ok(())
3111    }
3112
3113    fn delete_constraint(
3114        &mut self,
3115        new_ast: &mut ast::Node<ast::Program>,
3116        sketch: ObjectId,
3117        constraint_id: ObjectId,
3118    ) -> Result<(), KclError> {
3119        // Look up existing sketch.
3120        let sketch_id = sketch;
3121        let sketch_object = self
3122            .scene_graph
3123            .objects
3124            .get(sketch_id.0)
3125            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3126        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3127            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3128        };
3129        sketch
3130            .constraints
3131            .iter()
3132            .find(|o| **o == constraint_id)
3133            .ok_or_else(|| {
3134                KclError::refactor(format!(
3135                    "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
3136                ))
3137            })?;
3138        // Look up existing constraint.
3139        let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
3140            KclError::refactor(format!(
3141                "Constraint not found in scene graph: constraint={constraint_id:?}"
3142            ))
3143        })?;
3144        let ObjectKind::Constraint { .. } = &constraint_object.kind else {
3145            return Err(KclError::refactor(format!(
3146                "Object is not a constraint, it is {}",
3147                constraint_object.kind.human_friendly_kind_with_article()
3148            )));
3149        };
3150
3151        // Modify the AST to remove the constraint.
3152        self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
3153        Ok(())
3154    }
3155
3156    fn edit_coincident_constraint(
3157        &mut self,
3158        new_ast: &mut ast::Node<ast::Program>,
3159        constraint_id: ObjectId,
3160        segments: Vec<ConstraintSegment>,
3161    ) -> Result<(), KclError> {
3162        if segments.len() < 2 {
3163            return Err(KclError::refactor(format!(
3164                "Coincident constraint must have at least 2 inputs, got {}",
3165                segments.len()
3166            )));
3167        }
3168
3169        let segment_asts = segments
3170            .iter()
3171            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3172            .collect::<Result<Vec<_>, _>>()?;
3173
3174        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3175            elements: segment_asts,
3176            digest: None,
3177            non_code_meta: Default::default(),
3178        })));
3179
3180        self.mutate_ast(
3181            new_ast,
3182            constraint_id,
3183            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3184        )?;
3185        Ok(())
3186    }
3187
3188    fn edit_horizontal_points_constraint(
3189        &mut self,
3190        new_ast: &mut ast::Node<ast::Program>,
3191        constraint_id: ObjectId,
3192        points: Vec<ConstraintSegment>,
3193    ) -> Result<(), KclError> {
3194        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
3195    }
3196
3197    fn edit_vertical_points_constraint(
3198        &mut self,
3199        new_ast: &mut ast::Node<ast::Program>,
3200        constraint_id: ObjectId,
3201        points: Vec<ConstraintSegment>,
3202    ) -> Result<(), KclError> {
3203        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
3204    }
3205
3206    fn edit_axis_points_constraint(
3207        &mut self,
3208        new_ast: &mut ast::Node<ast::Program>,
3209        constraint_id: ObjectId,
3210        points: Vec<ConstraintSegment>,
3211        constraint_name: &str,
3212    ) -> Result<(), KclError> {
3213        if points.len() < 2 {
3214            return Err(KclError::refactor(format!(
3215                "{constraint_name} points constraint must have at least 2 points, got {}",
3216                points.len()
3217            )));
3218        }
3219
3220        let point_asts = points
3221            .iter()
3222            .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3223            .collect::<Result<Vec<_>, _>>()?;
3224
3225        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3226            elements: point_asts,
3227            digest: None,
3228            non_code_meta: Default::default(),
3229        })));
3230
3231        self.mutate_ast(
3232            new_ast,
3233            constraint_id,
3234            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3235        )?;
3236        Ok(())
3237    }
3238
3239    /// updates the equalLength constraint with the given lines
3240    fn edit_equal_length_constraint(
3241        &mut self,
3242        new_ast: &mut ast::Node<ast::Program>,
3243        constraint_id: ObjectId,
3244        lines: Vec<ObjectId>,
3245    ) -> Result<(), KclError> {
3246        if lines.len() < 2 {
3247            return Err(KclError::refactor(format!(
3248                "Lines equal length constraint must have at least 2 lines, got {}",
3249                lines.len()
3250            )));
3251        }
3252
3253        let line_asts = lines
3254            .iter()
3255            .map(|line_id| {
3256                let line_object = self
3257                    .scene_graph
3258                    .objects
3259                    .get(line_id.0)
3260                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3261                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3262                    let kind = line_object.kind.human_friendly_kind_with_article();
3263                    return Err(KclError::refactor(format!(
3264                        "This constraint only works on Segments, but you selected {kind}"
3265                    )));
3266                };
3267                let Segment::Line(_) = line_segment else {
3268                    let kind = line_segment.human_friendly_kind_with_article();
3269                    return Err(KclError::refactor(format!(
3270                        "Only lines can be made equal length, but you selected {kind}"
3271                    )));
3272                };
3273
3274                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3275            })
3276            .collect::<Result<Vec<_>, _>>()?;
3277
3278        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3279            elements: line_asts,
3280            digest: None,
3281            non_code_meta: Default::default(),
3282        })));
3283
3284        self.mutate_ast(
3285            new_ast,
3286            constraint_id,
3287            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3288        )?;
3289        Ok(())
3290    }
3291
3292    /// Updates the parallel constraint with the given lines.
3293    fn edit_parallel_constraint(
3294        &mut self,
3295        new_ast: &mut ast::Node<ast::Program>,
3296        constraint_id: ObjectId,
3297        lines: Vec<ObjectId>,
3298    ) -> Result<(), KclError> {
3299        if lines.len() < 2 {
3300            return Err(KclError::refactor(format!(
3301                "Parallel constraint must have at least 2 lines, got {}",
3302                lines.len()
3303            )));
3304        }
3305
3306        let line_asts = lines
3307            .iter()
3308            .map(|line_id| {
3309                let line_object = self
3310                    .scene_graph
3311                    .objects
3312                    .get(line_id.0)
3313                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3314                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3315                    let kind = line_object.kind.human_friendly_kind_with_article();
3316                    return Err(KclError::refactor(format!(
3317                        "This constraint only works on Segments, but you selected {kind}"
3318                    )));
3319                };
3320                let Segment::Line(_) = line_segment else {
3321                    let kind = line_segment.human_friendly_kind_with_article();
3322                    return Err(KclError::refactor(format!(
3323                        "Only lines can be made parallel, but you selected {kind}"
3324                    )));
3325                };
3326
3327                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3328            })
3329            .collect::<Result<Vec<_>, _>>()?;
3330
3331        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3332            elements: line_asts,
3333            digest: None,
3334            non_code_meta: Default::default(),
3335        })));
3336
3337        self.mutate_ast(
3338            new_ast,
3339            constraint_id,
3340            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3341        )?;
3342        Ok(())
3343    }
3344
3345    /// Updates the equalRadius constraint with the given segments.
3346    fn edit_equal_radius_constraint(
3347        &mut self,
3348        new_ast: &mut ast::Node<ast::Program>,
3349        constraint_id: ObjectId,
3350        input: Vec<ObjectId>,
3351    ) -> Result<(), KclError> {
3352        if input.len() < 2 {
3353            return Err(KclError::refactor(format!(
3354                "equalRadius constraint must have at least 2 segments, got {}",
3355                input.len()
3356            )));
3357        }
3358
3359        let input_asts = input
3360            .iter()
3361            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3362            .collect::<Result<Vec<_>, _>>()?;
3363
3364        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3365            elements: input_asts,
3366            digest: None,
3367            non_code_meta: Default::default(),
3368        })));
3369
3370        self.mutate_ast(
3371            new_ast,
3372            constraint_id,
3373            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3374        )?;
3375        Ok(())
3376    }
3377
3378    async fn execute_after_edit(
3379        &mut self,
3380        ctx: &ExecutorContext,
3381        sketch: ObjectId,
3382        sketch_block_ref: AstNodeRef,
3383        new_ast: &mut ast::Node<ast::Program>,
3384        options: ExecuteAfterEditOptions,
3385    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3386        let ExecuteAfterEditOptions {
3387            segment_ids_edited,
3388            edit_kind,
3389            commit_solved_initial_guesses,
3390        } = options;
3391
3392        // Convert to string source to create real source ranges.
3393        let new_source = source_from_ast(new_ast);
3394        // Parse the new KCL source.
3395        let new_program = parse_frontend_mutation_source(
3396            &new_source,
3397            "Error parsing KCL source after editing",
3398            "No AST produced after editing",
3399        )?;
3400
3401        // Truncate after the sketch block for mock execution.
3402        let is_delete = edit_kind.is_delete();
3403        let truncated_program = {
3404            let mut truncated_program = new_program.clone();
3405            only_sketch_block(
3406                &mut truncated_program.ast,
3407                &sketch_block_ref,
3408                edit_kind.to_change_kind(),
3409            )
3410            .map_err(KclErrorWithOutputs::no_outputs)?;
3411            truncated_program
3412        };
3413
3414        // Execute.
3415        let drag_anchors = self.next_segment_drag_anchors.take().unwrap_or_default();
3416        let mock_config = MockConfig {
3417            sketch_block_id: Some(sketch),
3418            freedom_analysis: is_delete,
3419            segment_ids_edited: segment_ids_edited.clone(),
3420            drag_anchors,
3421            ..Default::default()
3422        };
3423        let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
3424
3425        // Only now, after execution has succeeded, update self.program.
3426        self.program = new_program;
3427
3428        // Uses freedom_analysis: is_delete
3429        let outcome = self.update_state_after_exec(outcome, is_delete);
3430
3431        let src_delta = if commit_solved_initial_guesses {
3432            self.commit_var_solutions_to_program(&outcome, "editing")?
3433        } else {
3434            SourceDelta { text: new_source }
3435        };
3436        let scene_graph_delta = SceneGraphDelta {
3437            new_graph: self.scene_graph_for_ui(),
3438            invalidates_ids: is_delete,
3439            new_objects: Vec::new(),
3440            exec_outcome: outcome,
3441        };
3442        Ok((src_delta, scene_graph_delta))
3443    }
3444
3445    async fn execute_after_delete_sketch(
3446        &mut self,
3447        ctx: &ExecutorContext,
3448        new_ast: &mut ast::Node<ast::Program>,
3449    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3450        // Convert to string source to create real source ranges.
3451        let new_source = source_from_ast(new_ast);
3452        // Parse the new KCL source.
3453        let new_program = parse_frontend_mutation_source(
3454            &new_source,
3455            "Error parsing KCL source after editing",
3456            "No AST produced after editing",
3457        )?;
3458
3459        // Make sure to only set this if there are no errors.
3460        self.program = new_program.clone();
3461
3462        // We deleted the entire sketch block. It doesn't make sense to truncate
3463        // and execute only the sketch block. We execute the whole program with
3464        // a real engine.
3465
3466        // Execute.
3467        let outcome = ctx.run_with_caching(new_program).await?;
3468        let freedom_analysis_ran = true;
3469
3470        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
3471
3472        let src_delta = SourceDelta { text: new_source };
3473        let scene_graph_delta = SceneGraphDelta {
3474            new_graph: self.scene_graph_for_ui(),
3475            invalidates_ids: true,
3476            new_objects: Vec::new(),
3477            exec_outcome: outcome,
3478        };
3479        Ok((src_delta, scene_graph_delta))
3480    }
3481
3482    /// Map a point object id into an AST reference expression for use in
3483    /// constraints. If the point is owned by a segment (line or arc), we
3484    /// reference the appropriate property on that segment (e.g. `line1.start`,
3485    /// `arc1.center`). Otherwise we reference the point directly.
3486    fn point_id_to_ast_reference(
3487        &self,
3488        point_id: ObjectId,
3489        new_ast: &mut ast::Node<ast::Program>,
3490    ) -> Result<ast::Expr, KclError> {
3491        let point_object = self
3492            .scene_graph
3493            .objects
3494            .get(point_id.0)
3495            .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
3496        let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
3497            return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
3498        };
3499        let Segment::Point(point) = point_segment else {
3500            return Err(KclError::refactor(format!(
3501                "Only points are currently supported: {point_object:?}"
3502            )));
3503        };
3504
3505        if let Some(owner_id) = point.owner {
3506            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3507                KclError::refactor(format!(
3508                    "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
3509                ))
3510            })?;
3511            let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3512                return Err(KclError::refactor(format!(
3513                    "Owner of point is not a segment, but found {}",
3514                    owner_object.kind.human_friendly_kind_with_article()
3515                )));
3516            };
3517
3518            match owner_segment {
3519                Segment::Line(line) => {
3520                    let property = if line.start == point_id {
3521                        LINE_PROPERTY_START
3522                    } else if line.end == point_id {
3523                        LINE_PROPERTY_END
3524                    } else {
3525                        return Err(KclError::refactor(format!(
3526                            "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
3527                        )));
3528                    };
3529                    get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
3530                }
3531                Segment::Arc(arc) => {
3532                    let property = if arc.start == point_id {
3533                        ARC_PROPERTY_START
3534                    } else if arc.end == point_id {
3535                        ARC_PROPERTY_END
3536                    } else if arc.center == point_id {
3537                        ARC_PROPERTY_CENTER
3538                    } else {
3539                        return Err(KclError::refactor(format!(
3540                            "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
3541                        )));
3542                    };
3543                    get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
3544                }
3545                Segment::Circle(circle) => {
3546                    let property = if circle.start == point_id {
3547                        CIRCLE_PROPERTY_START
3548                    } else if circle.center == point_id {
3549                        CIRCLE_PROPERTY_CENTER
3550                    } else {
3551                        return Err(KclError::refactor(format!(
3552                            "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
3553                        )));
3554                    };
3555                    get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
3556                }
3557                Segment::ControlPointSpline(spline) => {
3558                    let Some(index) = spline.controls.iter().position(|id| *id == point_id) else {
3559                        return Err(KclError::refactor(format!(
3560                            "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
3561                        )));
3562                    };
3563                    let owner_expr =
3564                        get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3565                    let controls_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_CONTROLS);
3566                    Ok(create_index_expression(controls_expr, index))
3567                }
3568                _ => Err(KclError::refactor(format!(
3569                    "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
3570                ))),
3571            }
3572        } else {
3573            // Standalone point.
3574            get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
3575        }
3576    }
3577
3578    fn line_id_to_ast_reference(
3579        &self,
3580        line_id: ObjectId,
3581        new_ast: &mut ast::Node<ast::Program>,
3582    ) -> Result<ast::Expr, KclError> {
3583        let line_object = self
3584            .scene_graph
3585            .objects
3586            .get(line_id.0)
3587            .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3588        let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3589            return Err(KclError::refactor(format!("Object is not a segment: {line_object:?}")));
3590        };
3591        let Segment::Line(line) = line_segment else {
3592            return Err(KclError::refactor(format!(
3593                "Only lines are currently supported: {line_object:?}"
3594            )));
3595        };
3596
3597        if let Some(owner_id) = line.owner {
3598            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3599                KclError::refactor(format!(
3600                    "Owner of line not found in scene graph: line={line_id:?}, owner={owner_id:?}"
3601                ))
3602            })?;
3603            let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3604                return Err(KclError::refactor(format!(
3605                    "Owner of line is not a segment, but found {}",
3606                    owner_object.kind.human_friendly_kind_with_article()
3607                )));
3608            };
3609
3610            match owner_segment {
3611                Segment::ControlPointSpline(spline) => {
3612                    let Some(index) = spline
3613                        .controls
3614                        .windows(2)
3615                        .position(|window| window[0] == line.start && window[1] == line.end)
3616                    else {
3617                        return Err(KclError::refactor(format!(
3618                            "Internal: Line is not part of owner's controlPointSpline segment: line={line_id:?}, spline={owner_id:?}"
3619                        )));
3620                    };
3621                    let owner_expr =
3622                        get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3623                    let edges_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_EDGES);
3624                    Ok(create_index_expression(edges_expr, index))
3625                }
3626                _ => Err(KclError::refactor(format!(
3627                    "Internal: Owner of line is not a supported segment type for constraints: {owner_segment:?}"
3628                ))),
3629            }
3630        } else {
3631            get_or_insert_ast_reference(new_ast, &line_object.source, "line", None)
3632        }
3633    }
3634
3635    fn coincident_segment_to_ast(
3636        &self,
3637        segment: &ConstraintSegment,
3638        new_ast: &mut ast::Node<ast::Program>,
3639    ) -> Result<ast::Expr, KclError> {
3640        match segment {
3641            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3642            ConstraintSegment::Segment(segment_id) => self.segment_id_to_constraint_ast_reference(*segment_id, new_ast),
3643        }
3644    }
3645
3646    fn segment_id_to_constraint_ast_reference(
3647        &self,
3648        segment_id: ObjectId,
3649        new_ast: &mut ast::Node<ast::Program>,
3650    ) -> Result<ast::Expr, KclError> {
3651        let segment_object = self
3652            .scene_graph
3653            .objects
3654            .get(segment_id.0)
3655            .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
3656        let ObjectKind::Segment { segment } = &segment_object.kind else {
3657            return Err(KclError::refactor(format!(
3658                "Object is not a segment, it is {}",
3659                segment_object.kind.human_friendly_kind_with_article()
3660            )));
3661        };
3662
3663        match segment {
3664            Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3665            Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
3666            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None),
3667            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3668            Segment::ControlPointSpline(_) => {
3669                get_or_insert_ast_reference(new_ast, &segment_object.source, CONTROL_POINT_SPLINE_FN, None)
3670            }
3671        }
3672    }
3673
3674    fn axis_constraint_segment_to_ast(
3675        &self,
3676        segment: &ConstraintSegment,
3677        new_ast: &mut ast::Node<ast::Program>,
3678    ) -> Result<ast::Expr, KclError> {
3679        match segment {
3680            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3681            ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3682        }
3683    }
3684
3685    async fn add_coincident(
3686        &mut self,
3687        sketch: ObjectId,
3688        coincident: Coincident,
3689        new_ast: &mut ast::Node<ast::Program>,
3690    ) -> Result<AstNodeRef, KclError> {
3691        let sketch_id = sketch;
3692        for segment in &coincident.segments {
3693            let ConstraintSegment::Segment(segment_id) = segment else {
3694                continue;
3695            };
3696            let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
3697                continue;
3698            };
3699            if matches!(
3700                segment_object.kind,
3701                ObjectKind::Segment {
3702                    segment: Segment::ControlPointSpline(_)
3703                }
3704            ) {
3705                return Err(KclError::refactor(
3706                    "Coincident with a full controlPointSpline is not supported yet. Constrain a control point or spline edge instead."
3707                        .to_owned(),
3708                ));
3709            }
3710        }
3711        let segment_asts = coincident
3712            .segments
3713            .iter()
3714            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3715            .collect::<Result<Vec<_>, _>>()?;
3716        if segment_asts.len() < 2 {
3717            return Err(KclError::refactor(format!(
3718                "Coincident constraint must have at least 2 inputs, got {}",
3719                segment_asts.len()
3720            )));
3721        }
3722
3723        // Create the coincident() call using shared helper.
3724        let coincident_ast = create_coincident_ast(segment_asts);
3725
3726        // Add the line to the AST of the sketch block.
3727        let (sketch_block_ref, _) = self.mutate_ast(
3728            new_ast,
3729            sketch_id,
3730            AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3731        )?;
3732        Ok(sketch_block_ref)
3733    }
3734
3735    async fn add_distance(
3736        &mut self,
3737        sketch: ObjectId,
3738        distance: Distance,
3739        new_ast: &mut ast::Node<ast::Program>,
3740    ) -> Result<AstNodeRef, KclError> {
3741        self.add_distance_constraint(sketch, DISTANCE_FN, distance, new_ast)
3742    }
3743
3744    fn distance_constraint_ast_parts(
3745        &self,
3746        function_name: &str,
3747        distance: &Distance,
3748        new_ast: &mut ast::Node<ast::Program>,
3749    ) -> Result<(ast::BinaryPart, ast::BinaryPart), KclError> {
3750        let [segment0_ast, segment1_ast] = match distance.segments.as_slice() {
3751            [pt0, pt1] => [
3752                self.coincident_segment_to_ast(pt0, new_ast)?,
3753                self.coincident_segment_to_ast(pt1, new_ast)?,
3754            ],
3755            _ => {
3756                return Err(KclError::refactor(format!(
3757                    "Distance constraint must have exactly 2 segments, got {}",
3758                    distance.segments.len()
3759                )));
3760            }
3761        };
3762
3763        let arguments = match &distance.label_position {
3764            Some(label_position) => vec![ast::LabeledArg {
3765                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3766                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3767            }],
3768            None => Default::default(),
3769        };
3770
3771        let call = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
3772            callee: ast::Node::no_src(ast_sketch2_name(function_name)),
3773            unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
3774                ast::ArrayExpression {
3775                    elements: vec![segment0_ast, segment1_ast],
3776                    digest: None,
3777                    non_code_meta: Default::default(),
3778                },
3779            )))),
3780            arguments,
3781            digest: None,
3782            non_code_meta: Default::default(),
3783        })));
3784        let value = ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3785            value: ast::LiteralValue::Number {
3786                value: distance.distance.value,
3787                suffix: distance.distance.units,
3788            },
3789            raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3790                KclError::refactor(format!(
3791                    "Could not format numeric suffix: {:?}",
3792                    distance.distance.units
3793                ))
3794            })?,
3795            digest: None,
3796        })));
3797
3798        Ok((call, value))
3799    }
3800
3801    fn add_distance_constraint(
3802        &mut self,
3803        sketch: ObjectId,
3804        function_name: &str,
3805        distance: Distance,
3806        new_ast: &mut ast::Node<ast::Program>,
3807    ) -> Result<AstNodeRef, KclError> {
3808        let (call, value) = self.distance_constraint_ast_parts(function_name, &distance, new_ast)?;
3809        let distance_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
3810            left: call,
3811            operator: ast::BinaryOperator::Eq,
3812            right: value,
3813            digest: None,
3814        })));
3815
3816        let (sketch_block_ref, _) = self.mutate_ast(
3817            new_ast,
3818            sketch,
3819            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3820        )?;
3821        Ok(sketch_block_ref)
3822    }
3823
3824    async fn add_angle(
3825        &mut self,
3826        sketch: ObjectId,
3827        angle: Angle,
3828        new_ast: &mut ast::Node<ast::Program>,
3829    ) -> Result<AstNodeRef, KclError> {
3830        let sketch_id = sketch;
3831        let (angle_call_ast, angle_value_ast) = self.angle_constraint_ast_parts(&angle, new_ast)?;
3832        let angle_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
3833            left: angle_call_ast,
3834            operator: ast::BinaryOperator::Eq,
3835            right: angle_value_ast,
3836            digest: None,
3837        })));
3838
3839        // Add the line to the AST of the sketch block.
3840        let (sketch_block_ref, _) = self.mutate_ast(
3841            new_ast,
3842            sketch_id,
3843            AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3844        )?;
3845        Ok(sketch_block_ref)
3846    }
3847
3848    fn angle_constraint_ast_parts(
3849        &self,
3850        angle: &Angle,
3851        new_ast: &mut ast::Node<ast::Program>,
3852    ) -> Result<(ast::BinaryPart, ast::BinaryPart), KclError> {
3853        let &[l0_id, l1_id] = angle.lines.as_slice() else {
3854            return Err(KclError::refactor(format!(
3855                "Angle constraint must have exactly 2 lines, got {}",
3856                angle.lines.len()
3857            )));
3858        };
3859
3860        let l0_ast = self.line_id_to_ast_reference(l0_id, new_ast)?;
3861        let l1_ast = self.line_id_to_ast_reference(l1_id, new_ast)?;
3862        let lines_ast = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3863            elements: vec![l0_ast, l1_ast],
3864            digest: None,
3865            non_code_meta: Default::default(),
3866        })));
3867
3868        if angle.inverse == Some(true) && angle.sector.is_none() {
3869            return Err(KclError::refactor("Angle inverse requires an angle sector".to_owned()));
3870        }
3871
3872        let uses_angle_dimension = angle.sector.is_some();
3873        let mut arguments = if uses_angle_dimension {
3874            vec![ast::LabeledArg {
3875                label: Some(ast::Identifier::new(ANGLE_LINES_PARAM)),
3876                arg: lines_ast.clone(),
3877            }]
3878        } else {
3879            Default::default()
3880        };
3881
3882        if let Some(sector) = angle.sector {
3883            arguments.push(ast::LabeledArg {
3884                label: Some(ast::Identifier::new(ANGLE_SECTOR_PARAM)),
3885                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3886                    value: ast::LiteralValue::Number {
3887                        value: f64::from(sector),
3888                        suffix: NumericSuffix::None,
3889                    },
3890                    raw: sector.to_string(),
3891                    digest: None,
3892                }))),
3893            });
3894        }
3895
3896        if angle.inverse == Some(true) {
3897            arguments.push(ast::LabeledArg {
3898                label: Some(ast::Identifier::new(ANGLE_INVERSE_PARAM)),
3899                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3900                    value: ast::LiteralValue::Bool(true),
3901                    raw: true.to_string(),
3902                    digest: None,
3903                }))),
3904            });
3905        }
3906
3907        if let Some(label_position) = &angle.label_position {
3908            arguments.push(ast::LabeledArg {
3909                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3910                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3911            });
3912        }
3913
3914        let call = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
3915            callee: ast::Node::no_src(ast_sketch2_name(if uses_angle_dimension {
3916                ANGLE_DIMENSION_FN
3917            } else {
3918                ANGLE_FN
3919            })),
3920            unlabeled: (!uses_angle_dimension).then_some(lines_ast),
3921            arguments,
3922            digest: None,
3923            non_code_meta: Default::default(),
3924        })));
3925        let value = ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3926            value: ast::LiteralValue::Number {
3927                value: angle.angle.value,
3928                suffix: angle.angle.units,
3929            },
3930            raw: format_number_literal(angle.angle.value, angle.angle.units, None)
3931                .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units)))?,
3932            digest: None,
3933        })));
3934
3935        Ok((call, value))
3936    }
3937
3938    async fn add_tangent(
3939        &mut self,
3940        sketch: ObjectId,
3941        tangent: Tangent,
3942        new_ast: &mut ast::Node<ast::Program>,
3943    ) -> Result<AstNodeRef, KclError> {
3944        let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3945            return Err(KclError::refactor(format!(
3946                "Tangent constraint must have exactly 2 segments, got {}",
3947                tangent.input.len()
3948            )));
3949        };
3950        let sketch_id = sketch;
3951
3952        let seg0_object = self
3953            .scene_graph
3954            .objects
3955            .get(seg0_id.0)
3956            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3957        let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3958            return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3959        };
3960        let seg0_ast = match seg0_segment {
3961            Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3962                self.segment_id_to_constraint_ast_reference(seg0_id, new_ast)?
3963            }
3964            _ => {
3965                return Err(KclError::refactor(format!(
3966                    "Tangent supports only line/arc/circle segments for now, got: {seg0_segment:?}"
3967                )));
3968            }
3969        };
3970
3971        let seg1_object = self
3972            .scene_graph
3973            .objects
3974            .get(seg1_id.0)
3975            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3976        let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3977            return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3978        };
3979        let seg1_ast = match seg1_segment {
3980            Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3981                self.segment_id_to_constraint_ast_reference(seg1_id, new_ast)?
3982            }
3983            _ => {
3984                return Err(KclError::refactor(format!(
3985                    "Tangent supports only line/arc/circle segments for now, got: {seg1_segment:?}"
3986                )));
3987            }
3988        };
3989
3990        let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
3991        let (sketch_block_ref, _) = self.mutate_ast(
3992            new_ast,
3993            sketch_id,
3994            AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
3995        )?;
3996        Ok(sketch_block_ref)
3997    }
3998
3999    async fn add_symmetric(
4000        &mut self,
4001        sketch: ObjectId,
4002        symmetric: Symmetric,
4003        new_ast: &mut ast::Node<ast::Program>,
4004    ) -> Result<AstNodeRef, KclError> {
4005        let &[input0_id, input1_id] = symmetric.input.as_slice() else {
4006            return Err(KclError::refactor(format!(
4007                "Symmetric constraint must have exactly 2 inputs, got {}",
4008                symmetric.input.len()
4009            )));
4010        };
4011        let sketch_id = sketch;
4012
4013        let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
4014        let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
4015        let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
4016
4017        let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
4018        let (sketch_block_ref, _) = self.mutate_ast(
4019            new_ast,
4020            sketch_id,
4021            AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
4022        )?;
4023        Ok(sketch_block_ref)
4024    }
4025
4026    async fn add_midpoint(
4027        &mut self,
4028        sketch: ObjectId,
4029        midpoint: Midpoint,
4030        new_ast: &mut ast::Node<ast::Program>,
4031    ) -> Result<AstNodeRef, KclError> {
4032        let sketch_id = sketch;
4033        let point_ast = self.axis_constraint_segment_to_ast(&midpoint.point, new_ast)?;
4034
4035        let segment_object = self
4036            .scene_graph
4037            .objects
4038            .get(midpoint.segment.0)
4039            .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
4040        let ObjectKind::Segment {
4041            segment: midpoint_segment,
4042        } = &segment_object.kind
4043        else {
4044            return Err(KclError::refactor(format!(
4045                "Object must be a segment, but it was {}",
4046                segment_object.kind.human_friendly_kind_with_article()
4047            )));
4048        };
4049        let segment_ast = match midpoint_segment {
4050            Segment::Line(_) => self.line_id_to_ast_reference(midpoint.segment, new_ast)?,
4051            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
4052            _ => {
4053                return Err(KclError::refactor(format!(
4054                    "Midpoint target must be a line or arc segment but it was {}",
4055                    midpoint_segment.human_friendly_kind_with_article()
4056                )));
4057            }
4058        };
4059
4060        let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
4061        let (sketch_block_ref, _) = self.mutate_ast(
4062            new_ast,
4063            sketch_id,
4064            AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
4065        )?;
4066        Ok(sketch_block_ref)
4067    }
4068
4069    async fn add_equal_radius(
4070        &mut self,
4071        sketch: ObjectId,
4072        equal_radius: EqualRadius,
4073        new_ast: &mut ast::Node<ast::Program>,
4074    ) -> Result<AstNodeRef, KclError> {
4075        if equal_radius.input.len() < 2 {
4076            return Err(KclError::refactor(format!(
4077                "equalRadius constraint must have at least 2 segments, got {}",
4078                equal_radius.input.len()
4079            )));
4080        }
4081
4082        let sketch_id = sketch;
4083        let input_asts = equal_radius
4084            .input
4085            .iter()
4086            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
4087            .collect::<Result<Vec<_>, _>>()?;
4088
4089        let equal_radius_ast = create_equal_radius_ast(input_asts);
4090        let (sketch_block_ref, _) = self.mutate_ast(
4091            new_ast,
4092            sketch_id,
4093            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
4094        )?;
4095        Ok(sketch_block_ref)
4096    }
4097
4098    async fn add_radius(
4099        &mut self,
4100        sketch: ObjectId,
4101        radius: Radius,
4102        new_ast: &mut ast::Node<ast::Program>,
4103    ) -> Result<AstNodeRef, KclError> {
4104        let params = ArcSizeConstraintParams {
4105            points: vec![radius.arc],
4106            function_name: RADIUS_FN,
4107            value: radius.radius.value,
4108            units: radius.radius.units,
4109            label_position: radius.label_position,
4110            constraint_type_name: "Radius",
4111        };
4112        self.add_arc_size_constraint(sketch, params, new_ast).await
4113    }
4114
4115    async fn add_diameter(
4116        &mut self,
4117        sketch: ObjectId,
4118        diameter: Diameter,
4119        new_ast: &mut ast::Node<ast::Program>,
4120    ) -> Result<AstNodeRef, KclError> {
4121        let params = ArcSizeConstraintParams {
4122            points: vec![diameter.arc],
4123            function_name: DIAMETER_FN,
4124            value: diameter.diameter.value,
4125            units: diameter.diameter.units,
4126            label_position: diameter.label_position,
4127            constraint_type_name: "Diameter",
4128        };
4129        self.add_arc_size_constraint(sketch, params, new_ast).await
4130    }
4131
4132    async fn add_fixed_constraints(
4133        &mut self,
4134        sketch: ObjectId,
4135        points: Vec<FixedPoint>,
4136        new_ast: &mut ast::Node<ast::Program>,
4137    ) -> Result<AstNodeRef, KclError> {
4138        let mut sketch_block_ref = None;
4139
4140        for fixed_point in points {
4141            let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
4142            let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
4143                .map_err(|err| KclError::refactor(err.to_string()))?;
4144
4145            let (sketch_ref, _) = self.mutate_ast(
4146                new_ast,
4147                sketch,
4148                AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
4149            )?;
4150            sketch_block_ref = Some(sketch_ref);
4151        }
4152
4153        sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
4154    }
4155
4156    async fn add_arc_size_constraint(
4157        &mut self,
4158        sketch: ObjectId,
4159        params: ArcSizeConstraintParams,
4160        new_ast: &mut ast::Node<ast::Program>,
4161    ) -> Result<AstNodeRef, KclError> {
4162        let sketch_id = sketch;
4163
4164        // Constraint must have exactly 1 argument (arc segment)
4165        if params.points.len() != 1 {
4166            return Err(KclError::refactor(format!(
4167                "{} constraint must have exactly 1 argument (an arc segment), got {}",
4168                params.constraint_type_name,
4169                params.points.len()
4170            )));
4171        }
4172
4173        let arc_id = params.points[0];
4174        let arc_object = self
4175            .scene_graph
4176            .objects
4177            .get(arc_id.0)
4178            .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
4179        let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
4180            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
4181        };
4182        let ref_type = match arc_segment {
4183            Segment::Arc(_) => ARC_VARIABLE,
4184            Segment::Circle(_) => CIRCLE_VARIABLE,
4185            _ => {
4186                return Err(KclError::refactor(format!(
4187                    "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
4188                    params.constraint_type_name
4189                )));
4190            }
4191        };
4192        // Reference the arc/circle segment directly
4193        let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
4194        let arguments = match &params.label_position {
4195            Some(label_position) => vec![ast::LabeledArg {
4196                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4197                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4198            }],
4199            None => Default::default(),
4200        };
4201
4202        // Create the function call.
4203        let call_ast = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4204            callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
4205            unlabeled: Some(arc_ast),
4206            arguments,
4207            digest: None,
4208            non_code_meta: Default::default(),
4209        })));
4210        let constraint_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
4211            left: call_ast,
4212            operator: ast::BinaryOperator::Eq,
4213            right: ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
4214                value: ast::LiteralValue::Number {
4215                    value: params.value,
4216                    suffix: params.units,
4217                },
4218                raw: format_number_literal(params.value, params.units, None)
4219                    .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
4220                digest: None,
4221            }))),
4222            digest: None,
4223        })));
4224
4225        // Add the line to the AST of the sketch block.
4226        let (sketch_block_ref, _) = self.mutate_ast(
4227            new_ast,
4228            sketch_id,
4229            AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
4230        )?;
4231        Ok(sketch_block_ref)
4232    }
4233
4234    async fn add_horizontal_distance(
4235        &mut self,
4236        sketch: ObjectId,
4237        distance: Distance,
4238        new_ast: &mut ast::Node<ast::Program>,
4239    ) -> Result<AstNodeRef, KclError> {
4240        self.add_distance_constraint(sketch, HORIZONTAL_DISTANCE_FN, distance, new_ast)
4241    }
4242
4243    async fn add_vertical_distance(
4244        &mut self,
4245        sketch: ObjectId,
4246        distance: Distance,
4247        new_ast: &mut ast::Node<ast::Program>,
4248    ) -> Result<AstNodeRef, KclError> {
4249        self.add_distance_constraint(sketch, VERTICAL_DISTANCE_FN, distance, new_ast)
4250    }
4251
4252    async fn add_horizontal(
4253        &mut self,
4254        sketch: ObjectId,
4255        horizontal: Horizontal,
4256        new_ast: &mut ast::Node<ast::Program>,
4257    ) -> Result<AstNodeRef, KclError> {
4258        let sketch_id = sketch;
4259
4260        // Map the runtime objects back to variable names.
4261        let first_arg_ast = match horizontal {
4262            Horizontal::Line { line } => {
4263                let line_object = self
4264                    .scene_graph
4265                    .objects
4266                    .get(line.0)
4267                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4268                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4269                    let kind = line_object.kind.human_friendly_kind_with_article();
4270                    return Err(KclError::refactor(format!(
4271                        "This constraint only works on Segments, but you selected {kind}"
4272                    )));
4273                };
4274                let Segment::Line(_) = line_segment else {
4275                    return Err(KclError::refactor(format!(
4276                        "Only lines can be made horizontal, but you selected {}",
4277                        line_segment.human_friendly_kind_with_article(),
4278                    )));
4279                };
4280                self.line_id_to_ast_reference(line, new_ast)?
4281            }
4282            Horizontal::Points { points } => {
4283                let point_asts = points
4284                    .iter()
4285                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4286                    .collect::<Result<Vec<_>, _>>()?;
4287                ast::ArrayExpression::new(point_asts).into()
4288            }
4289        };
4290        // Create the horizontal() call using shared helper.
4291        let horizontal_ast = create_horizontal_ast(first_arg_ast);
4292
4293        // Add the line to the AST of the sketch block.
4294        let (sketch_block_ref, _) = self.mutate_ast(
4295            new_ast,
4296            sketch_id,
4297            AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
4298        )?;
4299        Ok(sketch_block_ref)
4300    }
4301
4302    async fn add_lines_equal_length(
4303        &mut self,
4304        sketch: ObjectId,
4305        lines_equal_length: LinesEqualLength,
4306        new_ast: &mut ast::Node<ast::Program>,
4307    ) -> Result<AstNodeRef, KclError> {
4308        if lines_equal_length.lines.len() < 2 {
4309            return Err(KclError::refactor(format!(
4310                "Lines equal length constraint must have at least 2 lines, got {}",
4311                lines_equal_length.lines.len()
4312            )));
4313        };
4314
4315        let sketch_id = sketch;
4316
4317        // Map the runtime objects back to variable names.
4318        let line_asts = lines_equal_length
4319            .lines
4320            .iter()
4321            .map(|line_id| {
4322                let line_object = self
4323                    .scene_graph
4324                    .objects
4325                    .get(line_id.0)
4326                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4327                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4328                    let kind = line_object.kind.human_friendly_kind_with_article();
4329                    return Err(KclError::refactor(format!(
4330                        "This constraint only works on Segments, but you selected {kind}"
4331                    )));
4332                };
4333                let Segment::Line(_) = line_segment else {
4334                    let kind = line_segment.human_friendly_kind_with_article();
4335                    return Err(KclError::refactor(format!(
4336                        "Only lines can be made equal length, but you selected {kind}"
4337                    )));
4338                };
4339
4340                self.line_id_to_ast_reference(*line_id, new_ast)
4341            })
4342            .collect::<Result<Vec<_>, _>>()?;
4343
4344        // Create the equalLength() call using shared helper.
4345        let equal_length_ast = create_equal_length_ast(line_asts);
4346
4347        // Add the constraint to the AST of the sketch block.
4348        let (sketch_block_ref, _) = self.mutate_ast(
4349            new_ast,
4350            sketch_id,
4351            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
4352        )?;
4353        Ok(sketch_block_ref)
4354    }
4355
4356    fn equal_radius_segment_id_to_ast_reference(
4357        &mut self,
4358        segment_id: ObjectId,
4359        new_ast: &mut ast::Node<ast::Program>,
4360    ) -> Result<ast::Expr, KclError> {
4361        let segment_object = self
4362            .scene_graph
4363            .objects
4364            .get(segment_id.0)
4365            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4366        let ObjectKind::Segment { segment } = &segment_object.kind else {
4367            return Err(KclError::refactor(format!(
4368                "Object is not a segment, it was {}",
4369                segment_object.kind.human_friendly_kind_with_article()
4370            )));
4371        };
4372
4373        let ref_type = match segment {
4374            Segment::Arc(_) => ARC_VARIABLE,
4375            Segment::Circle(_) => CIRCLE_VARIABLE,
4376            _ => {
4377                return Err(KclError::refactor(format!(
4378                    "equalRadius supports only arc/circle segments, got {}",
4379                    segment.human_friendly_kind_with_article()
4380                )));
4381            }
4382        };
4383
4384        get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
4385    }
4386
4387    fn symmetric_input_id_to_ast_reference(
4388        &mut self,
4389        segment_id: ObjectId,
4390        new_ast: &mut ast::Node<ast::Program>,
4391    ) -> Result<ast::Expr, KclError> {
4392        let segment_object = self
4393            .scene_graph
4394            .objects
4395            .get(segment_id.0)
4396            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4397        let ObjectKind::Segment { segment } = &segment_object.kind else {
4398            return Err(KclError::refactor(format!(
4399                "Object is not a segment, it was {}",
4400                segment_object.kind.human_friendly_kind_with_article()
4401            )));
4402        };
4403
4404        match segment {
4405            Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
4406            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
4407            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
4408            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
4409            Segment::ControlPointSpline(_) => Err(KclError::refactor(
4410                "Symmetric does not yet support control point splines".to_owned(),
4411            )),
4412        }
4413    }
4414
4415    fn symmetric_axis_id_to_ast_reference(
4416        &mut self,
4417        segment_id: ObjectId,
4418        new_ast: &mut ast::Node<ast::Program>,
4419    ) -> Result<ast::Expr, KclError> {
4420        let segment_object = self
4421            .scene_graph
4422            .objects
4423            .get(segment_id.0)
4424            .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
4425        let ObjectKind::Segment { segment } = &segment_object.kind else {
4426            return Err(KclError::refactor(format!(
4427                "Object is not a segment, it was {}",
4428                segment_object.kind.human_friendly_kind_with_article()
4429            )));
4430        };
4431        match segment {
4432            Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
4433            _ => Err(KclError::refactor(format!(
4434                "Symmetric axis must be a line, got {}",
4435                segment.human_friendly_kind_with_article()
4436            ))),
4437        }
4438    }
4439
4440    async fn add_parallel(
4441        &mut self,
4442        sketch: ObjectId,
4443        parallel: Parallel,
4444        new_ast: &mut ast::Node<ast::Program>,
4445    ) -> Result<AstNodeRef, KclError> {
4446        if parallel.lines.len() < 2 {
4447            return Err(KclError::refactor(format!(
4448                "Parallel constraint must have at least 2 lines, got {}",
4449                parallel.lines.len()
4450            )));
4451        };
4452
4453        let sketch_id = sketch;
4454
4455        let line_asts = parallel
4456            .lines
4457            .iter()
4458            .map(|line_id| {
4459                let line_object = self
4460                    .scene_graph
4461                    .objects
4462                    .get(line_id.0)
4463                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4464                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4465                    let kind = line_object.kind.human_friendly_kind_with_article();
4466                    return Err(KclError::refactor(format!(
4467                        "This constraint only works on Segments, but you selected {kind}"
4468                    )));
4469                };
4470                let Segment::Line(_) = line_segment else {
4471                    let kind = line_segment.human_friendly_kind_with_article();
4472                    return Err(KclError::refactor(format!(
4473                        "Only lines can be made parallel, but you selected {kind}"
4474                    )));
4475                };
4476
4477                self.line_id_to_ast_reference(*line_id, new_ast)
4478            })
4479            .collect::<Result<Vec<_>, _>>()?;
4480
4481        let call_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4482            callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
4483            unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
4484                ast::ArrayExpression {
4485                    elements: line_asts,
4486                    digest: None,
4487                    non_code_meta: Default::default(),
4488                },
4489            )))),
4490            arguments: Default::default(),
4491            digest: None,
4492            non_code_meta: Default::default(),
4493        })));
4494
4495        let (sketch_block_ref, _) = self.mutate_ast(
4496            new_ast,
4497            sketch_id,
4498            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4499        )?;
4500        Ok(sketch_block_ref)
4501    }
4502
4503    async fn add_perpendicular(
4504        &mut self,
4505        sketch: ObjectId,
4506        perpendicular: Perpendicular,
4507        new_ast: &mut ast::Node<ast::Program>,
4508    ) -> Result<AstNodeRef, KclError> {
4509        self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
4510            .await
4511    }
4512
4513    async fn add_lines_at_angle_constraint(
4514        &mut self,
4515        sketch: ObjectId,
4516        angle_kind: LinesAtAngleKind,
4517        lines: Vec<ObjectId>,
4518        new_ast: &mut ast::Node<ast::Program>,
4519    ) -> Result<AstNodeRef, KclError> {
4520        let &[line0_id, line1_id] = lines.as_slice() else {
4521            return Err(KclError::refactor(format!(
4522                "{} constraint must have exactly 2 lines, got {}",
4523                angle_kind.to_function_name(),
4524                lines.len()
4525            )));
4526        };
4527
4528        let sketch_id = sketch;
4529
4530        // Map the runtime objects back to variable names.
4531        let line0_object = self
4532            .scene_graph
4533            .objects
4534            .get(line0_id.0)
4535            .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
4536        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
4537            let kind = line0_object.kind.human_friendly_kind_with_article();
4538            return Err(KclError::refactor(format!(
4539                "This constraint only works on Segments, but you selected {kind}"
4540            )));
4541        };
4542        let Segment::Line(_) = line0_segment else {
4543            return Err(KclError::refactor(format!(
4544                "Only lines can be made {}, but you selected {}",
4545                angle_kind.to_function_name(),
4546                line0_segment.human_friendly_kind_with_article(),
4547            )));
4548        };
4549        let line0_ast = self.line_id_to_ast_reference(line0_id, new_ast)?;
4550
4551        let line1_object = self
4552            .scene_graph
4553            .objects
4554            .get(line1_id.0)
4555            .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
4556        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
4557            let kind = line1_object.kind.human_friendly_kind_with_article();
4558            return Err(KclError::refactor(format!(
4559                "This constraint only works on Segments, but you selected {kind}"
4560            )));
4561        };
4562        let Segment::Line(_) = line1_segment else {
4563            return Err(KclError::refactor(format!(
4564                "Only lines can be made {}, but you selected {}",
4565                angle_kind.to_function_name(),
4566                line1_segment.human_friendly_kind_with_article(),
4567            )));
4568        };
4569        let line1_ast = self.line_id_to_ast_reference(line1_id, new_ast)?;
4570
4571        // Create the parallel() or perpendicular() call.
4572        let call_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4573            callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
4574            unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
4575                ast::ArrayExpression {
4576                    elements: vec![line0_ast, line1_ast],
4577                    digest: None,
4578                    non_code_meta: Default::default(),
4579                },
4580            )))),
4581            arguments: Default::default(),
4582            digest: None,
4583            non_code_meta: Default::default(),
4584        })));
4585
4586        // Add the constraint to the AST of the sketch block.
4587        let (sketch_block_ref, _) = self.mutate_ast(
4588            new_ast,
4589            sketch_id,
4590            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4591        )?;
4592        Ok(sketch_block_ref)
4593    }
4594
4595    async fn add_vertical(
4596        &mut self,
4597        sketch: ObjectId,
4598        vertical: Vertical,
4599        new_ast: &mut ast::Node<ast::Program>,
4600    ) -> Result<AstNodeRef, KclError> {
4601        let sketch_id = sketch;
4602
4603        let first_arg_ast = match vertical {
4604            Vertical::Line { line } => {
4605                // Map the runtime objects back to variable names.
4606                let line_object = self
4607                    .scene_graph
4608                    .objects
4609                    .get(line.0)
4610                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4611                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4612                    let kind = line_object.kind.human_friendly_kind_with_article();
4613                    return Err(KclError::refactor(format!(
4614                        "This constraint only works on Segments, but you selected {kind}"
4615                    )));
4616                };
4617                let Segment::Line(_) = line_segment else {
4618                    return Err(KclError::refactor(format!(
4619                        "Only lines can be made vertical, but you selected {}",
4620                        line_segment.human_friendly_kind_with_article()
4621                    )));
4622                };
4623                self.line_id_to_ast_reference(line, new_ast)?
4624            }
4625            Vertical::Points { points } => {
4626                let point_asts = points
4627                    .iter()
4628                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4629                    .collect::<Result<Vec<_>, _>>()?;
4630                ast::ArrayExpression::new(point_asts).into()
4631            }
4632        };
4633        // Create the vertical() call using shared helper.
4634        let vertical_ast = create_vertical_ast(first_arg_ast);
4635
4636        // Add the line to the AST of the sketch block.
4637        let (sketch_block_ref, _) = self.mutate_ast(
4638            new_ast,
4639            sketch_id,
4640            AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4641        )?;
4642        Ok(sketch_block_ref)
4643    }
4644
4645    async fn execute_after_add_constraint(
4646        &mut self,
4647        ctx: &ExecutorContext,
4648        sketch_id: ObjectId,
4649        sketch_block_ref: AstNodeRef,
4650        new_ast: &mut ast::Node<ast::Program>,
4651    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4652        // Convert to string source to create real source ranges.
4653        let new_source = source_from_ast(new_ast);
4654        // Parse the new KCL source.
4655        let new_program = parse_frontend_mutation_source(
4656            &new_source,
4657            "Error parsing KCL source after adding constraint",
4658            "No AST produced after adding constraint",
4659        )?;
4660        let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4661            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4662                "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4663            )))
4664        })?;
4665
4666        // Truncate after the sketch block for mock execution.
4667        // Use a clone so we don't mutate new_program yet
4668        let mut truncated_program = new_program.clone();
4669        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4670            .map_err(KclErrorWithOutputs::no_outputs)?;
4671
4672        // Execute - if this fails, we haven't modified self yet, so state is safe
4673        let outcome = ctx
4674            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4675            .await?;
4676
4677        let new_object_ids = {
4678            // Extract the constraint ID from the execution outcome using source_range_to_object
4679            let constraint_id = outcome
4680                .source_range_to_object
4681                .get(&constraint_node_ref.range)
4682                .copied()
4683                .ok_or_else(|| {
4684                    KclErrorWithOutputs::from_error_outcome(
4685                        KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4686                        outcome.clone(),
4687                    )
4688                })?;
4689            vec![constraint_id]
4690        };
4691
4692        // Only now, after all operations succeeded, update self.program.
4693        // This ensures state is only modified if everything succeeds.
4694        self.program = new_program;
4695
4696        // Uses MockConfig::default() which has freedom_analysis: true
4697        let outcome = self.update_state_after_exec(outcome, true);
4698
4699        let src_delta = self.commit_var_solutions_to_program(&outcome, "adding constraint")?;
4700        let scene_graph_delta = SceneGraphDelta {
4701            new_graph: self.scene_graph_for_ui(),
4702            invalidates_ids: false,
4703            new_objects: new_object_ids,
4704            exec_outcome: outcome,
4705        };
4706        Ok((src_delta, scene_graph_delta))
4707    }
4708
4709    fn commit_var_solutions_to_program(&mut self, outcome: &ExecOutcome, operation: &str) -> ExecResult<SourceDelta> {
4710        let commit_failure = || {
4711            KclErrorWithOutputs::from_error_outcome(
4712                KclError::refactor(format!("Could not update KCL after {operation}.")),
4713                outcome.clone(),
4714            )
4715        };
4716
4717        let default_length_unit = self.default_length_unit();
4718        let mut settled_ast = self.program.ast.clone();
4719        let mut committed_solver_value = false;
4720        for (var_range, node_path, value) in &outcome.var_solutions {
4721            let Some(lookup) = numeric_literal_at_node_path(&settled_ast, node_path.as_ref(), *var_range) else {
4722                return Err(commit_failure());
4723            };
4724            let new_value = match &lookup {
4725                Some(current_literal) => {
4726                    if !var_solution_needs_commit(current_literal, *value, default_length_unit) {
4727                        continue;
4728                    }
4729                    preserve_var_solution_literal_style(current_literal, *value, default_length_unit)
4730                }
4731                None => {
4732                    // Bare `var` with no initial literal to compare against;
4733                    // always commit, using the module's default length unit as
4734                    // an explicit suffix so the written value carries units.
4735                    Number {
4736                        value: number_value_in_default_length_units(*value, default_length_unit),
4737                        units: default_length_unit.into(),
4738                    }
4739                }
4740            };
4741            committed_solver_value = true;
4742            let source_ref = SourceRef::Simple {
4743                range: *var_range,
4744                node_path: node_path.clone(),
4745            };
4746            mutate_ast_node_by_source_ref(
4747                &mut settled_ast,
4748                &source_ref,
4749                AstMutateCommand::EditVarInitialValue { value: new_value },
4750            )
4751            .map_err(|_| commit_failure())?;
4752        }
4753
4754        if !committed_solver_value {
4755            return Ok(SourceDelta {
4756                text: self.program.original_file_contents.clone(),
4757            });
4758        }
4759
4760        let settled_source = source_from_ast(&settled_ast);
4761        let (settled_program, errors) = Program::parse(&settled_source).map_err(|_| commit_failure())?;
4762        if !errors.is_empty() {
4763            return Err(commit_failure());
4764        }
4765        let Some(settled_program) = settled_program else {
4766            return Err(commit_failure());
4767        };
4768
4769        self.program = settled_program;
4770
4771        Ok(SourceDelta { text: settled_source })
4772    }
4773
4774    // Find constraints that reference the given segments.
4775    fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4776        if segment_ids_set.contains(&segment_id) {
4777            return true;
4778        }
4779
4780        let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4781            return false;
4782        };
4783        let ObjectKind::Segment { segment } = &segment_object.kind else {
4784            return false;
4785        };
4786        let Segment::Point(point) = segment else {
4787            return false;
4788        };
4789
4790        point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4791    }
4792
4793    fn remaining_constraint_segments(
4794        &self,
4795        segments: &[ConstraintSegment],
4796        segment_ids_set: &AhashIndexSet<ObjectId>,
4797    ) -> Vec<ConstraintSegment> {
4798        segments
4799            .iter()
4800            .copied()
4801            .filter(|segment| match segment {
4802                ConstraintSegment::Origin(_) => true,
4803                ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4804            })
4805            .collect()
4806    }
4807
4808    fn find_referenced_constraints(
4809        &self,
4810        sketch_id: ObjectId,
4811        segment_ids_set: &AhashIndexSet<ObjectId>,
4812    ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4813        // Look up the sketch.
4814        let sketch_object = self
4815            .scene_graph
4816            .objects
4817            .get(sketch_id.0)
4818            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4819        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4820            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4821        };
4822        let segment_or_owner_matches = |segment_id: ObjectId| {
4823            if segment_ids_set.contains(&segment_id) {
4824                return true;
4825            }
4826            let segment_object = self.scene_graph.objects.get(segment_id.0);
4827            if let Some(obj) = segment_object
4828                && let ObjectKind::Segment { segment } = &obj.kind
4829            {
4830                match segment {
4831                    Segment::Point(point) => point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4832                    Segment::Line(line) => line.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4833                    _ => false,
4834                }
4835            } else {
4836                false
4837            }
4838        };
4839        let mut constraint_ids_set = AhashIndexSet::default();
4840        for constraint_id in &sketch.constraints {
4841            let constraint_object = self
4842                .scene_graph
4843                .objects
4844                .get(constraint_id.0)
4845                .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4846            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4847                return Err(KclError::refactor(format!(
4848                    "Object is not a constraint, it is {}",
4849                    constraint_object.kind.human_friendly_kind_with_article()
4850                )));
4851            };
4852            let depends_on_segment = match constraint {
4853                Constraint::Coincident(c) => c.segment_ids().any(segment_or_owner_matches),
4854                Constraint::Distance(d) => d.segment_ids().any(segment_or_owner_matches),
4855                Constraint::Fixed(fixed) => fixed
4856                    .points
4857                    .iter()
4858                    .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4859                Constraint::Radius(r) => segment_or_owner_matches(r.arc),
4860                Constraint::Diameter(d) => segment_or_owner_matches(d.arc),
4861                Constraint::EqualRadius(equal_radius) => {
4862                    equal_radius.input.iter().copied().any(segment_or_owner_matches)
4863                }
4864                Constraint::HorizontalDistance(d) => d.segment_ids().any(segment_or_owner_matches),
4865                Constraint::VerticalDistance(d) => d.segment_ids().any(segment_or_owner_matches),
4866                Constraint::Horizontal(h) => match h {
4867                    Horizontal::Line { line } => segment_or_owner_matches(*line),
4868                    Horizontal::Points { points } => points.iter().any(|point| match point {
4869                        ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4870                        ConstraintSegment::Origin(_) => false,
4871                    }),
4872                },
4873                Constraint::Vertical(v) => match v {
4874                    Vertical::Line { line } => segment_or_owner_matches(*line),
4875                    Vertical::Points { points } => points.iter().any(|point| match point {
4876                        ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4877                        ConstraintSegment::Origin(_) => false,
4878                    }),
4879                },
4880                Constraint::LinesEqualLength(lines_equal_length) => {
4881                    lines_equal_length.lines.iter().copied().any(segment_or_owner_matches)
4882                }
4883                Constraint::Midpoint(midpoint) => {
4884                    segment_or_owner_matches(midpoint.segment)
4885                        || matches!(
4886                            midpoint.point,
4887                            ConstraintSegment::Segment(point) if segment_or_owner_matches(point)
4888                        )
4889                }
4890                Constraint::Parallel(parallel) => parallel.lines.iter().copied().any(segment_or_owner_matches),
4891                Constraint::Perpendicular(perpendicular) => {
4892                    perpendicular.lines.iter().copied().any(segment_or_owner_matches)
4893                }
4894                Constraint::Angle(angle) => angle.lines.iter().copied().any(segment_or_owner_matches),
4895                Constraint::Symmetric(symmetric) => {
4896                    segment_or_owner_matches(symmetric.axis)
4897                        || symmetric.input.iter().copied().any(segment_or_owner_matches)
4898                }
4899                Constraint::Tangent(tangent) => tangent.input.iter().copied().any(segment_or_owner_matches),
4900            };
4901            if depends_on_segment {
4902                constraint_ids_set.insert(*constraint_id);
4903            }
4904        }
4905        Ok(constraint_ids_set)
4906    }
4907
4908    fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4909        let mut outcome = outcome;
4910        self.solid_references = solid_references_from_variables(&self.program.ast, &outcome.variables);
4911        let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4912
4913        if freedom_analysis_ran {
4914            // When freedom analysis ran, replace the cache entirely with new values
4915            // Don't merge with old values since IDs might have changed
4916            self.point_freedom_cache.clear();
4917            for new_obj in &new_objects {
4918                if let ObjectKind::Segment {
4919                    segment: crate::front::Segment::Point(point),
4920                } = &new_obj.kind
4921                {
4922                    self.point_freedom_cache.insert(new_obj.id, point.freedom);
4923                }
4924            }
4925            add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4926            // Objects are already correct from the analysis, just use them as-is
4927            self.scene_graph.objects = new_objects;
4928        } else {
4929            // When freedom analysis didn't run, preserve old values and merge
4930            // Before replacing objects, extract and store freedom values from old objects
4931            for old_obj in &self.scene_graph.objects {
4932                if let ObjectKind::Segment {
4933                    segment: crate::front::Segment::Point(point),
4934                } = &old_obj.kind
4935                {
4936                    self.point_freedom_cache.insert(old_obj.id, point.freedom);
4937                }
4938            }
4939
4940            // Update objects, preserving stored freedom values when new is Free (might be default)
4941            let mut updated_objects = Vec::with_capacity(new_objects.len());
4942            for new_obj in new_objects {
4943                let mut obj = new_obj;
4944                if let ObjectKind::Segment {
4945                    segment: crate::front::Segment::Point(point),
4946                } = &mut obj.kind
4947                {
4948                    let new_freedom = point.freedom;
4949                    // When freedom_analysis=false, new values are defaults (Free).
4950                    // Only preserve cached values when new is Free (indicating it's a default, not from analysis).
4951                    // If new is NOT Free, use the new value (it came from somewhere else, maybe conflict detection).
4952                    // Never preserve Conflict from cache - conflicts are transient and should only be set
4953                    // when there are actually unsatisfied constraints.
4954                    match new_freedom {
4955                        Freedom::Free => {
4956                            match self.point_freedom_cache.get(&obj.id).copied() {
4957                                Some(Freedom::Conflict) => {
4958                                    // Don't preserve Conflict - conflicts are transient
4959                                    // Keep it as Free
4960                                }
4961                                Some(Freedom::Fixed) => {
4962                                    // Preserve Fixed cached value
4963                                    point.freedom = Freedom::Fixed;
4964                                }
4965                                Some(Freedom::Free) => {
4966                                    // If stored is also Free, keep Free (no change needed)
4967                                }
4968                                None => {
4969                                    // If no cached value, keep Free (default)
4970                                }
4971                            }
4972                        }
4973                        Freedom::Fixed => {
4974                            // Use new value (already set)
4975                        }
4976                        Freedom::Conflict => {
4977                            // Use new value (already set)
4978                        }
4979                    }
4980                    // Store the new freedom value (even if it's Free, so we know it was set)
4981                    self.point_freedom_cache.insert(obj.id, point.freedom);
4982                }
4983                updated_objects.push(obj);
4984            }
4985
4986            add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4987            self.scene_graph.objects = updated_objects;
4988        }
4989        outcome
4990    }
4991
4992    fn mutate_ast(
4993        &mut self,
4994        ast: &mut ast::Node<ast::Program>,
4995        object_id: ObjectId,
4996        command: AstMutateCommand,
4997    ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4998        let sketch_object = self
4999            .scene_graph
5000            .objects
5001            .get(object_id.0)
5002            .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
5003        mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
5004    }
5005
5006    fn mutate_constraint_label_position(
5007        &mut self,
5008        ast: &mut ast::Node<ast::Program>,
5009        constraint_id: ObjectId,
5010        label_position: Point2d<Number>,
5011    ) -> Result<(), KclError> {
5012        let object = self
5013            .scene_graph
5014            .objects
5015            .get(constraint_id.0)
5016            .ok_or_else(|| KclError::refactor(format!("Object not found: {constraint_id:?}")))?;
5017        if !matches!(
5018            &object.kind,
5019            ObjectKind::Constraint {
5020                constraint: Constraint::Distance(_)
5021                    | Constraint::HorizontalDistance(_)
5022                    | Constraint::VerticalDistance(_)
5023                    | Constraint::Radius(_)
5024                    | Constraint::Diameter(_)
5025                    | Constraint::Angle(_),
5026            }
5027        ) {
5028            return Err(KclError::refactor(format!(
5029                "Object does not support labelPosition: {constraint_id:?}"
5030            )));
5031        }
5032
5033        let label_position = to_ast_point2d_number(&label_position)
5034            .map_err(|err| KclError::refactor(format!("Could not convert label position to AST: {err}")))?;
5035        self.mutate_ast(
5036            ast,
5037            constraint_id,
5038            AstMutateCommand::EditDistanceConstraintLabelPosition { label_position },
5039        )?;
5040        Ok(())
5041    }
5042}
5043
5044fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
5045    // Look up existing sketch.
5046    let sketch_object = scene_graph
5047        .objects
5048        .get(sketch_id.0)
5049        .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
5050    let ObjectKind::Sketch(_) = &sketch_object.kind else {
5051        return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
5052    };
5053    expect_single_node_ref(sketch_object)
5054}
5055
5056fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
5057    match &object.source {
5058        SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
5059            range: *range,
5060            node_path: node_path.clone(),
5061        }),
5062        SourceRef::BackTrace { ranges } => {
5063            let [range] = ranges.as_slice() else {
5064                return Err(KclError::refactor(format!(
5065                    "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
5066                    ranges.len()
5067                )));
5068            };
5069            Ok(AstNodeRef {
5070                range: range.0,
5071                node_path: range.1.clone(),
5072            })
5073        }
5074    }
5075}
5076
5077/// This is a deprecated fall-back implementation. Prefer
5078/// [`only_sketch_block()`] to avoid reliance on source ranges.
5079fn only_sketch_block_from_range(
5080    ast: &mut ast::Node<ast::Program>,
5081    sketch_block_range: SourceRange,
5082    edit_kind: ChangeKind,
5083) -> Result<(), KclError> {
5084    let r1 = sketch_block_range;
5085    let matches_range = |r2: SourceRange| -> bool {
5086        // We may have added items to the sketch block, so the end may not be an
5087        // exact match.
5088        match edit_kind {
5089            ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
5090            // For edit, we don't know whether it grew or shrank.
5091            ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
5092            ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
5093            // No edit should be an exact match.
5094            ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
5095        }
5096    };
5097    let mut found = false;
5098    for item in ast.body.iter_mut() {
5099        match item {
5100            ast::BodyItem::ImportStatement(_) => {}
5101            ast::BodyItem::ExpressionStatement(node) => {
5102                if matches_range(SourceRange::from(&*node))
5103                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5104                {
5105                    sketch_block.is_being_edited = true;
5106                    found = true;
5107                    break;
5108                }
5109            }
5110            ast::BodyItem::VariableDeclaration(node) => {
5111                if matches_range(SourceRange::from(&node.declaration.init))
5112                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5113                {
5114                    sketch_block.is_being_edited = true;
5115                    found = true;
5116                    break;
5117                }
5118            }
5119            ast::BodyItem::TypeDeclaration(_) => {}
5120            ast::BodyItem::ReturnStatement(node) => {
5121                if matches_range(SourceRange::from(&node.argument))
5122                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5123                {
5124                    sketch_block.is_being_edited = true;
5125                    found = true;
5126                    break;
5127                }
5128            }
5129        }
5130    }
5131    if !found {
5132        return Err(KclError::refactor(format!(
5133            "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
5134        )));
5135    }
5136
5137    Ok(())
5138}
5139
5140fn only_sketch_block(
5141    ast: &mut ast::Node<ast::Program>,
5142    sketch_block_ref: &AstNodeRef,
5143    edit_kind: ChangeKind,
5144) -> Result<(), KclError> {
5145    let Some(target_node_path) = &sketch_block_ref.node_path else {
5146        #[cfg(target_arch = "wasm32")]
5147        web_sys::console::warn_1(
5148            &format!(
5149                "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
5150                sketch_block_ref
5151            )
5152            .into(),
5153        );
5154        return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
5155    };
5156    struct MarkSketchBlockBeingEdited<'a> {
5157        target_node_path: &'a ast::NodePath,
5158    }
5159
5160    impl Visitor for MarkSketchBlockBeingEdited<'_> {
5161        type Break = ();
5162        type Continue = ();
5163
5164        fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5165            if let NodeMut::SketchBlock(sketch_block) = node
5166                && sketch_block.node_path.as_ref() == Some(self.target_node_path)
5167            {
5168                sketch_block.is_being_edited = true;
5169                return TraversalReturn::new_break(());
5170            }
5171            TraversalReturn::new_continue(())
5172        }
5173
5174        fn finish(&mut self, _node: NodeMut<'_>) {}
5175    }
5176
5177    let mut marker = MarkSketchBlockBeingEdited { target_node_path };
5178    let found = dfs_mut(ast, &mut marker).is_break();
5179    if !found {
5180        return Err(KclError::refactor(format!(
5181            "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
5182        )));
5183    }
5184
5185    Ok(())
5186}
5187
5188fn sketch_on_ast_expr(
5189    ast: &mut ast::Node<ast::Program>,
5190    scene_graph: &SceneGraph,
5191    solid_references: &HashMap<Uuid, SolidAstReference>,
5192    on: &Plane,
5193) -> Result<ast::Expr, KclError> {
5194    match on {
5195        Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
5196        Plane::Object(object_id) => {
5197            let on_object = scene_graph
5198                .objects
5199                .get(object_id.0)
5200                .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
5201            if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
5202                return Ok(face_expr);
5203            }
5204            get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
5205        }
5206        Plane::PrimitiveFace(face) => {
5207            let solid_expr = solid_expr_for_engine_id(solid_references, face.solid_id).ok_or_else(|| {
5208                KclError::refactor(format!(
5209                    "Could not resolve a KCL solid for selected primitive face: solid_id={}",
5210                    face.solid_id
5211                ))
5212            })?;
5213            let face_id_expr = create_face_id_ast(solid_expr.clone(), face.index);
5214            Ok(create_face_of_ast(solid_expr, face_id_expr))
5215        }
5216    }
5217}
5218
5219fn solid_references_from_variables(
5220    ast: &ast::Node<ast::Program>,
5221    variables: &IndexMap<String, KclValueView>,
5222) -> HashMap<Uuid, SolidAstReference> {
5223    let mut references = HashMap::new();
5224
5225    // In-place operations such as shell reuse their input solid's engine ID.
5226    // Later variables overwrite earlier references so mutations target the result.
5227    for item in &ast.body {
5228        let ast::BodyItem::VariableDeclaration(declaration) = item else {
5229            continue;
5230        };
5231        let name = &declaration.declaration.id.name;
5232        let Some(value) = variables.get(name) else {
5233            continue;
5234        };
5235
5236        match value {
5237            KclValueView::Solid { value } => {
5238                references.insert(
5239                    value.id,
5240                    SolidAstReference {
5241                        variable_name: name.clone(),
5242                        output_index: None,
5243                    },
5244                );
5245            }
5246            KclValueView::Tuple { value } | KclValueView::HomArray { value } => {
5247                for (output_index, entry) in value.iter().enumerate() {
5248                    if let KclValueView::Solid { value } = entry {
5249                        references.insert(
5250                            value.id,
5251                            SolidAstReference {
5252                                variable_name: name.clone(),
5253                                output_index: Some(output_index),
5254                            },
5255                        );
5256                    }
5257                }
5258            }
5259            _ => {}
5260        }
5261    }
5262
5263    references
5264}
5265
5266fn solid_expr_for_engine_id(solid_references: &HashMap<Uuid, SolidAstReference>, solid_id: Uuid) -> Option<ast::Expr> {
5267    let reference = solid_references.get(&solid_id)?;
5268    let solid_expr = ast_name_expr(reference.variable_name.clone());
5269    Some(indexed_solid_expr_for_sweep_output(solid_expr, reference.output_index))
5270}
5271
5272fn sketch_face_of_scene_object_ast_expr(
5273    ast: &mut ast::Node<ast::Program>,
5274    on_object: &crate::front::Object,
5275) -> Result<Option<ast::Expr>, KclError> {
5276    match &on_object.kind {
5277        ObjectKind::Wall(wall) => {
5278            let solid_ref = get_or_insert_ast_reference(
5279                ast,
5280                &source_ref_from_source_ref_range(&wall.source.solid),
5281                "solid",
5282                None,
5283            )?;
5284            let ast::Expr::Name(solid_name_expr) = solid_ref else {
5285                return Err(KclError::refactor(format!(
5286                    "Could not resolve solid reference for selected wall: artifact_id={:?}",
5287                    on_object.artifact_id
5288                )));
5289            };
5290            let solid_expr = indexed_solid_expr_for_sweep_output(
5291                ast_name_expr(solid_name_expr.name.name.clone()),
5292                wall.solid_output_index,
5293            );
5294            let sweep_ref = get_or_insert_ast_reference(
5295                ast,
5296                &source_ref_from_source_ref_range(&wall.source.sweep),
5297                "solid",
5298                None,
5299            )?;
5300            let ast::Expr::Name(sweep_name_expr) = sweep_ref else {
5301                return Err(KclError::refactor(format!(
5302                    "Could not resolve sweep reference for selected wall: artifact_id={:?}",
5303                    on_object.artifact_id
5304                )));
5305            };
5306            let sweep_name = sweep_name_expr.name.name.clone();
5307            let segment_ref = get_or_insert_ast_reference(
5308                ast,
5309                &source_ref_from_source_ref_range(&wall.source.segment),
5310                LINE_VARIABLE,
5311                None,
5312            )?;
5313
5314            let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &sweep_name).or_else(|| {
5315                wall.source
5316                    .path
5317                    .as_ref()
5318                    .and_then(|path_source| region_name_from_path_source(ast, path_source))
5319            }) {
5320                let ast::Expr::Name(segment_name_expr) = segment_ref else {
5321                    return Err(KclError::refactor(format!(
5322                        "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
5323                        on_object.artifact_id
5324                    )));
5325                };
5326                create_member_expression(
5327                    create_member_expression(ast_name_expr(region_name), "tags"),
5328                    &segment_name_expr.name.name,
5329                )
5330            } else {
5331                segment_ref
5332            };
5333
5334            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5335        }
5336        ObjectKind::Cap(cap) => {
5337            let solid_ref =
5338                get_or_insert_ast_reference(ast, &source_ref_from_source_ref_range(&cap.source.solid), "solid", None)?;
5339            let ast::Expr::Name(solid_name_expr) = solid_ref else {
5340                return Err(KclError::refactor(format!(
5341                    "Could not resolve solid reference for selected cap: artifact_id={:?}",
5342                    on_object.artifact_id
5343                )));
5344            };
5345            let solid_expr = indexed_solid_expr_for_sweep_output(
5346                ast_name_expr(solid_name_expr.name.name.clone()),
5347                cap.solid_output_index,
5348            );
5349            // TODO: change this to explicit tag references with tagStart/tagEnd mutations
5350            let face_expr = match cap.kind {
5351                crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
5352                crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
5353            };
5354
5355            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5356        }
5357        _ => Ok(None),
5358    }
5359}
5360
5361fn indexed_solid_expr_for_sweep_output(solid_expr: ast::Expr, solid_output_index: Option<usize>) -> ast::Expr {
5362    match solid_output_index {
5363        Some(output_index) => create_index_expression(solid_expr, output_index),
5364        None => solid_expr,
5365    }
5366}
5367
5368fn source_ref_from_source_ref_range(source: &SourceRefRange) -> SourceRef {
5369    SourceRef::Simple {
5370        range: source.range,
5371        node_path: source.node_path.clone(),
5372    }
5373}
5374
5375fn region_name_from_path_source(ast: &ast::Node<ast::Program>, path_source: &SourceRefRange) -> Option<String> {
5376    let source_ref = source_ref_from_source_ref_range(path_source);
5377    let candidate = variable_name_containing_source_ref(ast, &source_ref)?;
5378    let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5379        return None;
5380    };
5381    let ast::Expr::CallExpressionKw(region_call) = &region_decl.init else {
5382        return None;
5383    };
5384    if region_call.callee.name.name != "region" {
5385        return None;
5386    }
5387    Some(candidate)
5388}
5389
5390fn downstream_composite_code_ref_for_source(artifact_graph: &ArtifactGraph, source_id: ArtifactId) -> Option<&CodeRef> {
5391    let mut current_id = source_id;
5392    let mut current_composite = None;
5393    let mut visited = HashSet::new();
5394
5395    while visited.insert(current_id) {
5396        let next_composite_id = downstream_composite_id_for_solid_source(artifact_graph, current_id);
5397
5398        let Some(composite_id) = next_composite_id else {
5399            break;
5400        };
5401        let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id) else {
5402            break;
5403        };
5404
5405        current_id = composite.id;
5406        current_composite = Some(composite);
5407
5408        if !composite.consumed {
5409            break;
5410        }
5411    }
5412
5413    current_composite.map(|composite| &composite.code_ref)
5414}
5415
5416fn downstream_composite_id_for_solid_source(
5417    artifact_graph: &ArtifactGraph,
5418    source_id: ArtifactId,
5419) -> Option<ArtifactId> {
5420    // Source is a path, find its solid.
5421    if let Some(Artifact::Path(path)) = artifact_graph.get(&source_id)
5422        && let Some(composite_id) = path.composite_solid_id
5423        && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5424        && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5425    {
5426        return Some(composite_id);
5427    }
5428
5429    // Source is a sweep, find its path -> then find the solid
5430    for artifact in artifact_graph.values() {
5431        if let Artifact::Path(path) = artifact
5432            && path.sweep_id == Some(source_id)
5433            && let Some(composite_id) = path.composite_solid_id
5434            && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5435            && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5436        {
5437            return Some(composite_id);
5438        }
5439    }
5440
5441    // Source is a solid, find its downstream solid.
5442    artifact_graph.values().find_map(|artifact| {
5443        let Artifact::CompositeSolid(composite) = artifact else {
5444            return None;
5445        };
5446        composite_contains_input(&composite.solid_ids, &composite.tool_ids, source_id).then_some(composite.id)
5447    })
5448}
5449
5450fn composite_contains_path_input(
5451    solid_ids: &[ArtifactId],
5452    tool_ids: &[ArtifactId],
5453    path_id: ArtifactId,
5454    solid2d_id: Option<ArtifactId>,
5455) -> bool {
5456    composite_contains_input(solid_ids, tool_ids, path_id)
5457        || solid2d_id.is_some_and(|solid2d_id| composite_contains_input(solid_ids, tool_ids, solid2d_id))
5458}
5459
5460fn composite_contains_input(solid_ids: &[ArtifactId], tool_ids: &[ArtifactId], input_id: ArtifactId) -> bool {
5461    solid_ids.contains(&input_id) || tool_ids.contains(&input_id)
5462}
5463
5464fn code_ref_source_ref_range(code_ref: &CodeRef) -> SourceRefRange {
5465    let node_path = (!code_ref.node_path.is_empty()).then(|| code_ref.node_path.clone());
5466    SourceRefRange {
5467        range: code_ref.range,
5468        node_path,
5469    }
5470}
5471
5472fn solid_output_index_for_sweep(
5473    artifact_graph: &ArtifactGraph,
5474    sweep_id: ArtifactId,
5475    sweep_code_ref: &CodeRef,
5476) -> Option<usize> {
5477    // Constituent sweeps are implementation details of one composite body,
5478    // even when cloning gives them the same CodeRef as the composite root.
5479    if downstream_composite_id_for_solid_source(artifact_graph, sweep_id).is_some() {
5480        return None;
5481    }
5482
5483    let sibling_sweeps = artifact_graph
5484        .values()
5485        .filter_map(|artifact| match artifact {
5486            Artifact::Sweep(sweep)
5487                if sweep.code_ref.range == sweep_code_ref.range
5488                    && sweep.code_ref.node_path == sweep_code_ref.node_path =>
5489            {
5490                Some(sweep)
5491            }
5492            _ => None,
5493        })
5494        .collect::<Vec<_>>();
5495
5496    if sibling_sweeps.len() <= 1 {
5497        return None;
5498    }
5499
5500    sibling_sweeps
5501        .iter()
5502        .position(|sibling_sweep| sibling_sweep.id == sweep_id)
5503}
5504
5505fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
5506    let mut existing_artifact_ids = scene_objects
5507        .iter()
5508        .map(|object| object.artifact_id)
5509        .collect::<HashSet<_>>();
5510
5511    for artifact in artifact_graph.values() {
5512        match artifact {
5513            Artifact::Wall(wall) => {
5514                if existing_artifact_ids.contains(&wall.id) {
5515                    continue;
5516                }
5517
5518                let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
5519                    Artifact::Segment(segment) => Some(segment),
5520                    _ => None,
5521                }) else {
5522                    continue;
5523                };
5524                let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
5525                    Artifact::Sweep(sweep) => Some(sweep),
5526                    _ => None,
5527                }) else {
5528                    continue;
5529                };
5530                let source_segment = segment
5531                    .original_seg_id
5532                    .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
5533                    .and_then(|artifact| match artifact {
5534                        Artifact::Segment(segment) => Some(segment),
5535                        _ => None,
5536                    })
5537                    .unwrap_or(segment);
5538                let solid_code_ref =
5539                    downstream_composite_code_ref_for_source(artifact_graph, wall.sweep_id).unwrap_or(&sweep.code_ref);
5540                let path_code_ref = artifact_graph
5541                    .get(&segment.path_id)
5542                    .or_else(|| artifact_graph.get(&sweep.path_id))
5543                    .and_then(|artifact| match artifact {
5544                        Artifact::Path(path) => Some(&path.code_ref),
5545                        _ => None,
5546                    });
5547                let source = WallSource {
5548                    solid: code_ref_source_ref_range(solid_code_ref),
5549                    sweep: code_ref_source_ref_range(&sweep.code_ref),
5550                    path: path_code_ref.map(code_ref_source_ref_range),
5551                    segment: code_ref_source_ref_range(&source_segment.code_ref),
5552                };
5553                let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5554                    && solid_code_ref.node_path == sweep.code_ref.node_path)
5555                    .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5556                    .flatten();
5557                let object_source = source_ref_from_source_ref_range(&source.solid);
5558                let id = ObjectId(scene_objects.len());
5559                scene_objects.push(crate::front::Object {
5560                    id,
5561                    kind: ObjectKind::Wall(crate::frontend::api::Wall {
5562                        id,
5563                        source,
5564                        solid_output_index,
5565                    }),
5566                    label: Default::default(),
5567                    comments: Default::default(),
5568                    artifact_id: wall.id,
5569                    source: object_source,
5570                });
5571                existing_artifact_ids.insert(wall.id);
5572            }
5573            Artifact::Cap(cap) => {
5574                if existing_artifact_ids.contains(&cap.id) {
5575                    continue;
5576                }
5577
5578                let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
5579                    Artifact::Sweep(sweep) => Some(sweep),
5580                    _ => None,
5581                }) else {
5582                    continue;
5583                };
5584                let id = ObjectId(scene_objects.len());
5585                let kind = match cap.sub_type {
5586                    CapSubType::Start => crate::frontend::api::CapKind::Start,
5587                    CapSubType::End => crate::frontend::api::CapKind::End,
5588                };
5589                let solid_code_ref =
5590                    downstream_composite_code_ref_for_source(artifact_graph, cap.sweep_id).unwrap_or(&sweep.code_ref);
5591                let source = CapSource {
5592                    solid: code_ref_source_ref_range(solid_code_ref),
5593                    sweep: code_ref_source_ref_range(&sweep.code_ref),
5594                };
5595                let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5596                    && solid_code_ref.node_path == sweep.code_ref.node_path)
5597                    .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5598                    .flatten();
5599                let object_source = source_ref_from_source_ref_range(&source.solid);
5600                scene_objects.push(crate::front::Object {
5601                    id,
5602                    kind: ObjectKind::Cap(crate::frontend::api::Cap {
5603                        id,
5604                        kind,
5605                        source,
5606                        solid_output_index,
5607                    }),
5608                    label: Default::default(),
5609                    comments: Default::default(),
5610                    artifact_id: cap.id,
5611                    source: object_source,
5612                });
5613                existing_artifact_ids.insert(cap.id);
5614            }
5615            _ => {}
5616        }
5617    }
5618}
5619
5620fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
5621    use crate::engine::PlaneName;
5622
5623    match name {
5624        PlaneName::Xy => ast_name_expr("XY".to_owned()),
5625        PlaneName::Xz => ast_name_expr("XZ".to_owned()),
5626        PlaneName::Yz => ast_name_expr("YZ".to_owned()),
5627        PlaneName::NegXy => negated_plane_ast_expr("XY"),
5628        PlaneName::NegXz => negated_plane_ast_expr("XZ"),
5629        PlaneName::NegYz => negated_plane_ast_expr("YZ"),
5630    }
5631}
5632
5633fn negated_plane_ast_expr(name: &str) -> ast::Expr {
5634    ast::Expr::UnaryExpression(BoxNode::new(ast::UnaryExpression::new(
5635        ast::UnaryOperator::Neg,
5636        ast::BinaryPart::Name(BoxNode::new(ast_name(name.to_owned()))),
5637    )))
5638}
5639
5640fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
5641    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
5642        callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
5643        unlabeled: Some(solid_expr),
5644        arguments: vec![ast::LabeledArg {
5645            label: Some(ast::Identifier::new("face")),
5646            arg: face_expr,
5647        }],
5648        digest: None,
5649        non_code_meta: Default::default(),
5650    })))
5651}
5652
5653fn create_face_id_ast(solid_expr: ast::Expr, index: usize) -> ast::Expr {
5654    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
5655        callee: ast::Node::no_src(ast_sketch2_name("faceId")),
5656        unlabeled: Some(solid_expr),
5657        arguments: vec![ast::LabeledArg {
5658            label: Some(ast::Identifier::new("index")),
5659            arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(
5660                ast::NumericLiteral {
5661                    value: index as f64,
5662                    suffix: NumericSuffix::None,
5663                    raw: index.to_string(),
5664                    digest: None,
5665                },
5666            )))),
5667        }],
5668        digest: None,
5669        non_code_meta: Default::default(),
5670    })))
5671}
5672
5673fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
5674    let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
5675        return None;
5676    };
5677    let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
5678        return None;
5679    };
5680    if !matches!(
5681        sweep_call.callee.name.name.as_str(),
5682        "extrude" | "revolve" | "sweep" | "loft"
5683    ) {
5684        return None;
5685    }
5686    let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
5687        return None;
5688    };
5689    let candidate = region_name_expr.name.name.clone();
5690    let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5691        return None;
5692    };
5693    let ast::Expr::CallExpressionKw(region_call) = &region_decl.init else {
5694        return None;
5695    };
5696    if region_call.callee.name.name != "region" {
5697        return None;
5698    }
5699    Some(candidate)
5700}
5701
5702/// Return the AST expression referencing the variable at the given source ref.
5703/// If no such variable exists, insert a new variable declaration with the given
5704/// prefix.
5705///
5706/// This may return a complex expression referencing properties of the variable
5707/// (e.g., `line1.start`).
5708fn get_or_insert_ast_reference(
5709    ast: &mut ast::Node<ast::Program>,
5710    source_ref: &SourceRef,
5711    prefix: &str,
5712    property: Option<&str>,
5713) -> Result<ast::Expr, KclError> {
5714    let command = AstMutateCommand::AddVariableDeclaration {
5715        prefix: prefix.to_owned(),
5716    };
5717    let ret = match mutate_ast_node_by_source_ref(ast, source_ref, command) {
5718        Ok((_, ret)) => ret,
5719        Err(err) => {
5720            if let Some(var_name) = variable_name_containing_source_ref(ast, source_ref) {
5721                AstMutateCommandReturn::Name(var_name)
5722            } else {
5723                return Err(err);
5724            }
5725        }
5726    };
5727    let AstMutateCommandReturn::Name(var_name) = ret else {
5728        return Err(KclError::refactor(
5729            "Expected variable name returned from AddVariableDeclaration".to_owned(),
5730        ));
5731    };
5732    let var_expr = ast::Expr::Name(BoxNode::new(ast::Name::new(&var_name)));
5733    let Some(property) = property else {
5734        // No property; just return the variable name.
5735        return Ok(var_expr);
5736    };
5737
5738    Ok(create_member_expression(var_expr, property))
5739}
5740
5741fn variable_name_containing_source_ref(ast: &ast::Node<ast::Program>, source_ref: &SourceRef) -> Option<String> {
5742    let source_range = match source_ref {
5743        SourceRef::Simple { range, .. } => *range,
5744        SourceRef::BackTrace { ranges } => {
5745            let [range] = ranges.as_slice() else {
5746                return None;
5747            };
5748            range.0
5749        }
5750    };
5751    ast.body.iter().find_map(|item| {
5752        let ast::BodyItem::VariableDeclaration(var_decl) = item else {
5753            return None;
5754        };
5755        let init_range = SourceRange::from(&var_decl.declaration.init);
5756        let source_is_inside_init = init_range.module_id() == source_range.module_id()
5757            && init_range.start() <= source_range.start()
5758            && source_range.end() <= init_range.end();
5759        if matches!(&var_decl.declaration.init, ast::Expr::SketchBlock(_))
5760            && init_range != source_range
5761            && source_is_inside_init
5762        {
5763            return None;
5764        }
5765        source_is_inside_init.then(|| var_decl.name().to_owned())
5766    })
5767}
5768
5769fn mutate_ast_node_by_source_ref(
5770    ast: &mut ast::Node<ast::Program>,
5771    source_ref: &SourceRef,
5772    command: AstMutateCommand,
5773) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5774    let (source_range, node_path) = match source_ref {
5775        SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
5776        SourceRef::BackTrace { ranges } => {
5777            let [range] = ranges.as_slice() else {
5778                return Err(KclError::refactor(format!(
5779                    "Expected single source ref, got {}; ranges={ranges:#?}",
5780                    ranges.len(),
5781                )));
5782            };
5783            (range.0, range.1.clone())
5784        }
5785    };
5786    let mut context = AstMutateContext {
5787        source_range,
5788        node_path,
5789        command,
5790        defined_names_stack: Default::default(),
5791    };
5792    let control = dfs_mut(ast, &mut context);
5793    match control {
5794        ControlFlow::Continue(_) => Err(KclError::refactor(
5795            "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
5796        )),
5797        ControlFlow::Break(break_value) => break_value,
5798    }
5799}
5800
5801#[derive(Debug)]
5802struct AstMutateContext {
5803    source_range: SourceRange,
5804    node_path: Option<ast::NodePath>,
5805    command: AstMutateCommand,
5806    defined_names_stack: Vec<HashSet<String>>,
5807}
5808
5809#[derive(Debug)]
5810#[allow(clippy::large_enum_variant)]
5811enum AstMutateCommand {
5812    /// Add an expression statement to the sketch block.
5813    AddSketchBlockExprStmt {
5814        expr: ast::Expr,
5815    },
5816    /// Add a variable declaration to the sketch block (e.g. `line1 = line(...)`).
5817    AddSketchBlockVarDecl {
5818        prefix: String,
5819        expr: ast::Expr,
5820    },
5821    AddVariableDeclaration {
5822        prefix: String,
5823    },
5824    EditPoint {
5825        at: ast::Expr,
5826    },
5827    EditLine {
5828        start: ast::Expr,
5829        end: ast::Expr,
5830        construction: Option<bool>,
5831    },
5832    EditArc {
5833        start: ast::Expr,
5834        end: ast::Expr,
5835        center: ast::Expr,
5836        direction: Option<ArcDirection>,
5837        construction: Option<bool>,
5838    },
5839    EditCircle {
5840        start: ast::Expr,
5841        center: ast::Expr,
5842        construction: Option<bool>,
5843    },
5844    EditControlPointSpline {
5845        points: ast::Expr,
5846        construction: Option<bool>,
5847    },
5848    EditConstraintValue {
5849        value: ast::BinaryPart,
5850    },
5851    EditAngleConstraint {
5852        call: ast::BinaryPart,
5853        value: ast::BinaryPart,
5854    },
5855    EditDistanceConstraint {
5856        call: ast::BinaryPart,
5857        value: ast::BinaryPart,
5858    },
5859    EditDistanceConstraintLabelPosition {
5860        label_position: ast::Expr,
5861    },
5862    EditCallUnlabeled {
5863        arg: ast::Expr,
5864    },
5865    EditVarInitialValue {
5866        value: Number,
5867    },
5868    DeleteNode,
5869}
5870
5871impl AstMutateCommand {
5872    fn needs_defined_names_stack(&self) -> bool {
5873        matches!(
5874            self,
5875            AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
5876        )
5877    }
5878}
5879
5880#[derive(Debug)]
5881enum AstMutateCommandReturn {
5882    None,
5883    Name(String),
5884}
5885
5886#[derive(Debug, Clone)]
5887struct AstNodeRef {
5888    range: SourceRange,
5889    node_path: Option<ast::NodePath>,
5890}
5891
5892impl<T> From<&ast::Node<T>> for AstNodeRef {
5893    fn from(value: &ast::Node<T>) -> Self {
5894        AstNodeRef {
5895            range: value.into(),
5896            node_path: value.node_path.clone(),
5897        }
5898    }
5899}
5900
5901impl From<&ast::BodyItem> for AstNodeRef {
5902    fn from(value: &ast::BodyItem) -> Self {
5903        match value {
5904            ast::BodyItem::ImportStatement(node) => AstNodeRef {
5905                range: node.into(),
5906                node_path: node.node_path.clone(),
5907            },
5908            ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
5909                range: node.into(),
5910                node_path: node.node_path.clone(),
5911            },
5912            ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
5913                range: node.into(),
5914                node_path: node.node_path.clone(),
5915            },
5916            ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
5917                range: node.into(),
5918                node_path: node.node_path.clone(),
5919            },
5920            ast::BodyItem::ReturnStatement(node) => AstNodeRef {
5921                range: node.into(),
5922                node_path: node.node_path.clone(),
5923            },
5924        }
5925    }
5926}
5927
5928impl From<&ast::Expr> for AstNodeRef {
5929    fn from(value: &ast::Expr) -> Self {
5930        AstNodeRef {
5931            range: SourceRange::from(value),
5932            node_path: value.node_path().cloned(),
5933        }
5934    }
5935}
5936
5937impl From<&AstMutateContext> for AstNodeRef {
5938    fn from(value: &AstMutateContext) -> Self {
5939        AstNodeRef {
5940            range: value.source_range,
5941            node_path: value.node_path.clone(),
5942        }
5943    }
5944}
5945
5946impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5947    type Error = crate::walk::AstNodeError;
5948
5949    fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5950        Ok(AstNodeRef {
5951            range: SourceRange::try_from(value)?,
5952            node_path: value.try_into()?,
5953        })
5954    }
5955}
5956
5957impl From<AstNodeRef> for SourceRange {
5958    fn from(value: AstNodeRef) -> Self {
5959        value.range
5960    }
5961}
5962
5963impl Visitor for AstMutateContext {
5964    type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5965    type Continue = ();
5966
5967    fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5968        filter_and_process(self, node)
5969    }
5970
5971    fn finish(&mut self, node: NodeMut<'_>) {
5972        match &node {
5973            NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
5974                self.defined_names_stack.pop();
5975            }
5976            _ => {}
5977        }
5978    }
5979}
5980
5981fn filter_and_process(
5982    ctx: &mut AstMutateContext,
5983    node: NodeMut,
5984) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
5985    let Ok(node_range) = SourceRange::try_from(&node) else {
5986        // Nodes that can't be converted to a range aren't interesting.
5987        return TraversalReturn::new_continue(());
5988    };
5989    // If we're adding a variable declaration, we need to look at variable
5990    // declaration expressions to see if it already has a variable, before
5991    // continuing. The variable declaration's source range won't match the
5992    // target; its init expression will.
5993    if let NodeMut::VariableDeclaration(var_decl) = &node {
5994        let expr_range = SourceRange::from(&var_decl.declaration.init);
5995        let expr_node_path = var_decl.declaration.init.node_path();
5996        if source_ref_matches(ctx, expr_range, expr_node_path) {
5997            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5998                // We found the variable declaration expression. It doesn't need
5999                // to be added.
6000                return TraversalReturn::new_break(Ok((
6001                    AstNodeRef::from(&**var_decl),
6002                    AstMutateCommandReturn::Name(var_decl.name().to_owned()),
6003                )));
6004            }
6005            if let AstMutateCommand::DeleteNode = &ctx.command {
6006                // We found the variable declaration. Delete the variable along
6007                // with the segment.
6008                return TraversalReturn {
6009                    mutate_body_item: MutateBodyItem::Delete,
6010                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
6011                };
6012            }
6013        }
6014    }
6015    // Similar thing with expression statement. We need to look at the
6016    // expression inside it.
6017    if let NodeMut::ExpressionStatement(expr_stmt) = &node {
6018        let expr_range = SourceRange::from(&expr_stmt.expression);
6019        let expr_node_path = expr_stmt.expression.node_path();
6020        if source_ref_matches(ctx, expr_range, expr_node_path) {
6021            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
6022                // We found the node wrapped in an expression statement. Process
6023                // the statement.
6024                let Ok(node_ref) = AstNodeRef::try_from(&node) else {
6025                    return TraversalReturn::new_continue(());
6026                };
6027                return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
6028            }
6029            if let AstMutateCommand::DeleteNode = &ctx.command {
6030                // We found the node wrapped in an expression statement. Delete
6031                // the whole statement.
6032                return TraversalReturn {
6033                    mutate_body_item: MutateBodyItem::Delete,
6034                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
6035                };
6036            }
6037        }
6038    }
6039
6040    if ctx.command.needs_defined_names_stack() {
6041        if let NodeMut::Program(program) = &node {
6042            ctx.defined_names_stack.push(find_defined_names(*program));
6043        } else if let NodeMut::SketchBlock(block) = &node {
6044            ctx.defined_names_stack.push(find_defined_names(&block.body));
6045        }
6046    }
6047
6048    // Make sure the node matches the source ref.
6049    let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
6050    if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
6051        return TraversalReturn::new_continue(());
6052    }
6053    let Ok(node_ref) = AstNodeRef::try_from(&node) else {
6054        return TraversalReturn::new_continue(());
6055    };
6056    process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
6057}
6058
6059fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
6060    match &ctx.node_path {
6061        Some(target) => Some(target) == node_path,
6062        None => node_range == ctx.source_range,
6063    }
6064}
6065
6066fn is_angle_constraint_call_name(name: &str) -> bool {
6067    matches!(name, ANGLE_FN | ANGLE_DIMENSION_FN)
6068}
6069
6070fn is_distance_constraint_call_name(name: &str) -> bool {
6071    matches!(name, DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN)
6072}
6073
6074fn is_constraint_call_name(name: &str) -> bool {
6075    matches!(
6076        name,
6077        DISTANCE_FN
6078            | HORIZONTAL_DISTANCE_FN
6079            | VERTICAL_DISTANCE_FN
6080            | RADIUS_FN
6081            | DIAMETER_FN
6082            | ANGLE_FN
6083            | ANGLE_DIMENSION_FN
6084    )
6085}
6086
6087fn constraint_supports_label_position(part: &mut ast::BinaryPart) -> Option<&mut BoxNode<CallExpressionKw>> {
6088    if let ast::BinaryPart::CallExpressionKw(call) = part
6089        && is_constraint_call_name(call.callee.name.name.as_str())
6090    {
6091        Some(call)
6092    } else {
6093        None
6094    }
6095}
6096
6097fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
6098    match &ctx.command {
6099        AstMutateCommand::AddSketchBlockExprStmt { expr } => {
6100            if let NodeMut::SketchBlock(sketch_block) = node {
6101                sketch_block
6102                    .body
6103                    .items
6104                    .push(ast::BodyItem::ExpressionStatement(ast::Node {
6105                        inner: ast::ExpressionStatement {
6106                            expression: expr.clone(),
6107                            digest: None,
6108                        },
6109                        start: Default::default(),
6110                        end: Default::default(),
6111                        module_id: Default::default(),
6112                        node_path: None,
6113                        outer_attrs: Default::default(),
6114                        pre_comments: Default::default(),
6115                        comment_start: Default::default(),
6116                    }));
6117                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6118            }
6119        }
6120        AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
6121            if let NodeMut::SketchBlock(sketch_block) = node {
6122                let empty_defined_names = HashSet::new();
6123                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6124                let Ok(name) = next_free_name(prefix, defined_names) else {
6125                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6126                };
6127                sketch_block
6128                    .body
6129                    .items
6130                    .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
6131                        ast::VariableDeclaration::new(
6132                            ast::VariableDeclarator::new(&name, expr.clone()),
6133                            ast::ItemVisibility::Default,
6134                            ast::VariableKind::Const,
6135                        ),
6136                    ))));
6137                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
6138            }
6139        }
6140        AstMutateCommand::AddVariableDeclaration { prefix } => {
6141            if let NodeMut::VariableDeclaration(inner) = node {
6142                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
6143            }
6144            if let NodeMut::ExpressionStatement(expr_stmt) = node {
6145                let empty_defined_names = HashSet::new();
6146                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6147                let Ok(name) = next_free_name(prefix, defined_names) else {
6148                    // TODO: Return an error instead?
6149                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6150                };
6151                let mutate_node =
6152                    ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(ast::VariableDeclaration::new(
6153                        ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
6154                        ast::ItemVisibility::Default,
6155                        ast::VariableKind::Const,
6156                    ))));
6157                return TraversalReturn {
6158                    mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
6159                    control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
6160                };
6161            }
6162        }
6163        AstMutateCommand::EditPoint { at } => {
6164            if let NodeMut::CallExpressionKw(call) = node {
6165                if call.callee.name.name != POINT_FN {
6166                    return TraversalReturn::new_continue(());
6167                }
6168                // Update the arguments.
6169                for labeled_arg in &mut call.arguments {
6170                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
6171                        labeled_arg.arg = at.clone();
6172                    }
6173                }
6174                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6175            }
6176        }
6177        AstMutateCommand::EditLine {
6178            start,
6179            end,
6180            construction,
6181        } => {
6182            if let NodeMut::CallExpressionKw(call) = node {
6183                if call.callee.name.name != LINE_FN {
6184                    return TraversalReturn::new_continue(());
6185                }
6186                // Update the arguments.
6187                for labeled_arg in &mut call.arguments {
6188                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
6189                        labeled_arg.arg = start.clone();
6190                    }
6191                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
6192                        labeled_arg.arg = end.clone();
6193                    }
6194                }
6195                // Handle construction kwarg
6196                if let Some(construction_value) = construction {
6197                    let construction_exists = call
6198                        .arguments
6199                        .iter()
6200                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6201                    if *construction_value {
6202                        // Add or update construction=true
6203                        if construction_exists {
6204                            // Update existing construction kwarg
6205                            for labeled_arg in &mut call.arguments {
6206                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6207                                    labeled_arg.arg =
6208                                        ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6209                                            value: ast::LiteralValue::Bool(true),
6210                                            raw: "true".to_string(),
6211                                            digest: None,
6212                                        })));
6213                                }
6214                            }
6215                        } else {
6216                            // Add new construction kwarg
6217                            call.arguments.push(ast::LabeledArg {
6218                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6219                                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6220                                    value: ast::LiteralValue::Bool(true),
6221                                    raw: "true".to_string(),
6222                                    digest: None,
6223                                }))),
6224                            });
6225                        }
6226                    } else {
6227                        // Remove construction kwarg if it exists
6228                        call.arguments
6229                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6230                    }
6231                }
6232                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6233            }
6234        }
6235        AstMutateCommand::EditArc {
6236            start,
6237            end,
6238            center,
6239            direction,
6240            construction,
6241        } => {
6242            if let NodeMut::CallExpressionKw(call) = node {
6243                if call.callee.name.name != ARC_FN {
6244                    return TraversalReturn::new_continue(());
6245                }
6246                // Update the arguments.
6247                for labeled_arg in &mut call.arguments {
6248                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
6249                        labeled_arg.arg = start.clone();
6250                    }
6251                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
6252                        labeled_arg.arg = end.clone();
6253                    }
6254                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
6255                        labeled_arg.arg = center.clone();
6256                    }
6257                }
6258                // Handle direction kwarg
6259                if let Some(direction_value) = direction {
6260                    let direction_exists = call
6261                        .arguments
6262                        .iter()
6263                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_DIRECTION_PARAM));
6264                    if direction_value.is_clockwise() {
6265                        let direction_ast = ast::Expr::Name(BoxNode::new(ast::Name::new(ARC_DIRECTION_CW_NAME)));
6266                        if direction_exists {
6267                            // Update existing direction kwarg
6268                            for labeled_arg in &mut call.arguments {
6269                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_DIRECTION_PARAM) {
6270                                    labeled_arg.arg = direction_ast.clone();
6271                                }
6272                            }
6273                        } else {
6274                            // Add new direction kwarg
6275                            call.arguments.push(ast::LabeledArg {
6276                                label: Some(ast::Identifier::new(ARC_DIRECTION_PARAM)),
6277                                arg: direction_ast,
6278                            });
6279                        }
6280                    } else {
6281                        // Remove direction kwarg if it exists since
6282                        // counterclockwise is the default
6283                        call.arguments
6284                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(ARC_DIRECTION_PARAM));
6285                    }
6286                }
6287                // Handle construction kwarg
6288                if let Some(construction_value) = construction {
6289                    let construction_exists = call
6290                        .arguments
6291                        .iter()
6292                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6293                    if *construction_value {
6294                        // Add or update construction=true
6295                        if construction_exists {
6296                            // Update existing construction kwarg
6297                            for labeled_arg in &mut call.arguments {
6298                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6299                                    labeled_arg.arg =
6300                                        ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6301                                            value: ast::LiteralValue::Bool(true),
6302                                            raw: "true".to_string(),
6303                                            digest: None,
6304                                        })));
6305                                }
6306                            }
6307                        } else {
6308                            // Add new construction kwarg
6309                            call.arguments.push(ast::LabeledArg {
6310                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6311                                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6312                                    value: ast::LiteralValue::Bool(true),
6313                                    raw: "true".to_string(),
6314                                    digest: None,
6315                                }))),
6316                            });
6317                        }
6318                    } else {
6319                        // Remove construction kwarg if it exists
6320                        call.arguments
6321                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6322                    }
6323                }
6324                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6325            }
6326        }
6327        AstMutateCommand::EditCircle {
6328            start,
6329            center,
6330            construction,
6331        } => {
6332            if let NodeMut::CallExpressionKw(call) = node {
6333                if call.callee.name.name != CIRCLE_FN {
6334                    return TraversalReturn::new_continue(());
6335                }
6336                // Update the arguments.
6337                for labeled_arg in &mut call.arguments {
6338                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
6339                        labeled_arg.arg = start.clone();
6340                    }
6341                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
6342                        labeled_arg.arg = center.clone();
6343                    }
6344                }
6345                // Handle construction kwarg
6346                if let Some(construction_value) = construction {
6347                    let construction_exists = call
6348                        .arguments
6349                        .iter()
6350                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6351                    if *construction_value {
6352                        if construction_exists {
6353                            // Update existing construction kwarg
6354                            for labeled_arg in &mut call.arguments {
6355                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6356                                    labeled_arg.arg =
6357                                        ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6358                                            value: ast::LiteralValue::Bool(true),
6359                                            raw: "true".to_string(),
6360                                            digest: None,
6361                                        })));
6362                                }
6363                            }
6364                        } else {
6365                            // Add new construction kwarg
6366                            call.arguments.push(ast::LabeledArg {
6367                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6368                                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6369                                    value: ast::LiteralValue::Bool(true),
6370                                    raw: "true".to_string(),
6371                                    digest: None,
6372                                }))),
6373                            });
6374                        }
6375                    } else {
6376                        // Remove construction kwarg if it exists
6377                        call.arguments
6378                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6379                    }
6380                }
6381                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6382            }
6383        }
6384        AstMutateCommand::EditControlPointSpline { points, construction } => {
6385            if let NodeMut::CallExpressionKw(call) = node {
6386                if call.callee.name.name != CONTROL_POINT_SPLINE_FN {
6387                    return TraversalReturn::new_continue(());
6388                }
6389                for labeled_arg in &mut call.arguments {
6390                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONTROL_POINT_SPLINE_POINTS_PARAM)
6391                    {
6392                        labeled_arg.arg = points.clone();
6393                    }
6394                }
6395                // Handle construction kwarg
6396                if let Some(construction_value) = construction {
6397                    let construction_exists = call
6398                        .arguments
6399                        .iter()
6400                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6401                    if *construction_value {
6402                        if construction_exists {
6403                            for labeled_arg in &mut call.arguments {
6404                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6405                                    labeled_arg.arg =
6406                                        ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6407                                            value: ast::LiteralValue::Bool(true),
6408                                            raw: "true".to_string(),
6409                                            digest: None,
6410                                        })));
6411                                }
6412                            }
6413                        } else {
6414                            call.arguments.push(ast::LabeledArg {
6415                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6416                                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6417                                    value: ast::LiteralValue::Bool(true),
6418                                    raw: "true".to_string(),
6419                                    digest: None,
6420                                }))),
6421                            });
6422                        }
6423                    } else {
6424                        call.arguments
6425                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6426                    }
6427                }
6428                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6429            }
6430        }
6431        AstMutateCommand::EditConstraintValue { value } => {
6432            if let NodeMut::BinaryExpression(binary_expr) = node {
6433                let left_is_constraint = matches!(
6434                    &binary_expr.left,
6435                    ast::BinaryPart::CallExpressionKw(call) if is_constraint_call_name(call.callee.name.name.as_str())
6436                );
6437                if left_is_constraint {
6438                    binary_expr.right = value.clone();
6439                } else {
6440                    binary_expr.left = value.clone();
6441                }
6442
6443                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6444            }
6445        }
6446        AstMutateCommand::EditAngleConstraint { call, value } => {
6447            if let NodeMut::BinaryExpression(binary_expr) = node {
6448                let left_is_angle = matches!(
6449                    &binary_expr.left,
6450                    ast::BinaryPart::CallExpressionKw(existing_call)
6451                        if is_angle_constraint_call_name(existing_call.callee.name.name.as_str())
6452                );
6453                let right_is_angle = matches!(
6454                    &binary_expr.right,
6455                    ast::BinaryPart::CallExpressionKw(existing_call)
6456                        if is_angle_constraint_call_name(existing_call.callee.name.name.as_str())
6457                );
6458
6459                match (left_is_angle, right_is_angle) {
6460                    (true, _) => {
6461                        binary_expr.left = call.clone();
6462                        binary_expr.right = value.clone();
6463                    }
6464                    (false, true) => {
6465                        binary_expr.left = value.clone();
6466                        binary_expr.right = call.clone();
6467                    }
6468                    (false, false) => return TraversalReturn::new_continue(()),
6469                }
6470
6471                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6472            }
6473        }
6474        AstMutateCommand::EditDistanceConstraint { call, value } => {
6475            if let NodeMut::BinaryExpression(binary_expr) = node {
6476                let left_is_distance = matches!(
6477                    &binary_expr.left,
6478                    ast::BinaryPart::CallExpressionKw(existing_call)
6479                        if is_distance_constraint_call_name(existing_call.callee.name.name.as_str())
6480                );
6481                let right_is_distance = matches!(
6482                    &binary_expr.right,
6483                    ast::BinaryPart::CallExpressionKw(existing_call)
6484                        if is_distance_constraint_call_name(existing_call.callee.name.name.as_str())
6485                );
6486
6487                match (left_is_distance, right_is_distance) {
6488                    (true, _) => {
6489                        binary_expr.left = call.clone();
6490                        binary_expr.right = value.clone();
6491                    }
6492                    (false, true) => {
6493                        binary_expr.left = value.clone();
6494                        binary_expr.right = call.clone();
6495                    }
6496                    (false, false) => return TraversalReturn::new_continue(()),
6497                }
6498
6499                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6500            }
6501        }
6502        AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
6503            if let NodeMut::BinaryExpression(binary_expr) = node {
6504                let call = if let Some(call) = constraint_supports_label_position(&mut binary_expr.left) {
6505                    call
6506                } else if let Some(call) = constraint_supports_label_position(&mut binary_expr.right) {
6507                    call
6508                } else {
6509                    return TraversalReturn::new_continue(());
6510                };
6511
6512                if let Some(label_arg) = call
6513                    .arguments
6514                    .iter_mut()
6515                    .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
6516                {
6517                    label_arg.arg = label_position.clone();
6518                } else {
6519                    call.arguments.push(ast::LabeledArg {
6520                        label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
6521                        arg: label_position.clone(),
6522                    });
6523                }
6524
6525                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6526            }
6527        }
6528        AstMutateCommand::EditCallUnlabeled { arg } => {
6529            if let NodeMut::CallExpressionKw(call) = node {
6530                call.unlabeled = Some(arg.clone());
6531                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6532            }
6533        }
6534        AstMutateCommand::EditVarInitialValue { value } => {
6535            // We target the SketchVar itself (matched by NodePath) rather than
6536            // the inner NumericLiteral so we can also write back into vars that
6537            // were declared without an initial value (e.g. bare `var`).
6538            if let NodeMut::SketchVar(sketch_var) = node {
6539                let Ok(literal) = to_source_number(*value) else {
6540                    return TraversalReturn::new_break(Err(KclError::refactor(format!(
6541                        "Could not convert number to AST literal: {:?}",
6542                        *value
6543                    ))));
6544                };
6545                sketch_var.initial = Some(BoxNode::new(ast::Node::no_src(literal)));
6546                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6547            }
6548        }
6549        AstMutateCommand::DeleteNode => {
6550            return TraversalReturn {
6551                mutate_body_item: MutateBodyItem::Delete,
6552                control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
6553            };
6554        }
6555    }
6556    TraversalReturn::new_continue(())
6557}
6558
6559struct FindSketchBlockSourceRange {
6560    /// The source range of the sketch block before mutation.
6561    target_before_mutation: SourceRange,
6562    /// The source range of the sketch block's last body item after mutation. We
6563    /// need to use a [Cell] since the [crate::walk::Visitor] trait requires a
6564    /// shared reference.
6565    found: Cell<Option<AstNodeRef>>,
6566}
6567
6568impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
6569    type Error = crate::front::Error;
6570
6571    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6572        let Ok(node_range) = SourceRange::try_from(&node) else {
6573            return Ok(true);
6574        };
6575
6576        if let crate::walk::Node::SketchBlock(sketch_block) = node {
6577            if node_range.module_id() == self.target_before_mutation.module_id()
6578                && node_range.start() == self.target_before_mutation.start()
6579                // End shouldn't match since we added something.
6580                && node_range.end() >= self.target_before_mutation.end()
6581            {
6582                self.found.set(sketch_block.body.items.last().map(|item| match item {
6583                    // For declarations like `circle1 = circle(...)`, use
6584                    // the init expression range so lookup in source_range_to_object
6585                    // matches the segment source range.
6586                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6587                    _ => AstNodeRef::from(item),
6588                }));
6589                return Ok(false);
6590            } else {
6591                // We found a different sketch block. No need to descend into
6592                // its children since sketch blocks cannot be nested.
6593                return Ok(true);
6594            }
6595        }
6596
6597        for child in node.children().iter() {
6598            if !child.visit(*self)? {
6599                return Ok(false);
6600            }
6601        }
6602
6603        Ok(true)
6604    }
6605}
6606
6607struct FindSketchBlockByNodePath {
6608    /// The Node Path of the sketch block before mutation.
6609    target_node_path: ast::NodePath,
6610    /// The ref of the sketch block's last body item after mutation. We need to
6611    /// use a [Cell] since the [crate::walk::Visitor] trait requires a shared
6612    /// reference.
6613    found: Cell<Option<AstNodeRef>>,
6614}
6615
6616impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
6617    type Error = crate::front::Error;
6618
6619    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6620        let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
6621            return Ok(true);
6622        };
6623
6624        if let crate::walk::Node::SketchBlock(sketch_block) = node {
6625            if let Some(node_path) = node_path
6626                && node_path == self.target_node_path
6627            {
6628                self.found.set(sketch_block.body.items.last().map(|item| match item {
6629                    // For declarations like `circle1 = circle(...)`, use
6630                    // the init expression range so lookup in source_range_to_object
6631                    // matches the segment source range.
6632                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6633                    _ => AstNodeRef::from(item),
6634                }));
6635
6636                return Ok(false);
6637            } else {
6638                // We found a different sketch block. No need to descend into
6639                // its children since sketch blocks cannot be nested.
6640                return Ok(true);
6641            }
6642        }
6643
6644        for child in node.children().iter() {
6645            if !child.visit(*self)? {
6646                return Ok(false);
6647            }
6648        }
6649
6650        Ok(true)
6651    }
6652}
6653
6654/// After adding an item to a sketch block, find the sketch block, and get the
6655/// source range of the added item. We assume that the added item is the last
6656/// item in the sketch block and that the sketch block's source range has grown,
6657/// but not moved from its starting offset.
6658///
6659/// TODO: Do we need to format *before* mutation in case formatting moves the
6660/// sketch block forward?
6661fn find_sketch_block_added_item(
6662    ast: &ast::Node<ast::Program>,
6663    sketch_block_before_mutation: &AstNodeRef,
6664) -> Result<AstNodeRef, KclError> {
6665    if let Some(node_path) = &sketch_block_before_mutation.node_path {
6666        let find = FindSketchBlockByNodePath {
6667            target_node_path: node_path.clone(),
6668            found: Cell::new(None),
6669        };
6670        let node = crate::walk::Node::from(ast);
6671        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6672        find.found.into_inner().ok_or_else(|| {
6673            KclError::refactor(format!(
6674                "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
6675            ))
6676        })
6677    } else {
6678        // No NodePath. Fall back to legacy source range.
6679        let find = FindSketchBlockSourceRange {
6680            target_before_mutation: sketch_block_before_mutation.range,
6681            found: Cell::new(None),
6682        };
6683        let node = crate::walk::Node::from(ast);
6684        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6685        find.found.into_inner().ok_or_else(|| KclError::refactor(
6686            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?"),
6687        ))
6688    }
6689}
6690
6691fn format_kcl_error_message(prefix: &str, error: &KclError) -> String {
6692    let message = error.message().trim();
6693    let message = if message.is_empty() {
6694        "unknown parse error"
6695    } else {
6696        message
6697    };
6698
6699    format!("{prefix}: {message}")
6700}
6701
6702fn parse_frontend_mutation_source(source: &str, parse_error_prefix: &str, no_ast_message: &str) -> ExecResult<Program> {
6703    let (program, errors) = Program::parse(source).map_err(|err| {
6704        KclErrorWithOutputs::no_outputs(KclError::refactor(format_kcl_error_message(parse_error_prefix, &err)))
6705    })?;
6706    if !errors.is_empty() {
6707        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
6708            format_compilation_issues(parse_error_prefix, &errors),
6709        )));
6710    }
6711
6712    program.ok_or_else(|| KclErrorWithOutputs::no_outputs(KclError::refactor(no_ast_message.to_owned())))
6713}
6714
6715fn format_compilation_issues(prefix: &str, issues: &[CompilationIssue]) -> String {
6716    let Some(first_issue) = issues
6717        .iter()
6718        .find(|issue| issue.severity.is_err())
6719        .or_else(|| issues.first())
6720    else {
6721        return prefix.to_owned();
6722    };
6723
6724    let message = first_issue.message.trim();
6725    let message = if message.is_empty() {
6726        "unknown parse error"
6727    } else {
6728        message
6729    };
6730
6731    if issues.len() > 1 {
6732        format!("{prefix}: {message} (+{} more)", issues.len() - 1)
6733    } else {
6734        format!("{prefix}: {message}")
6735    }
6736}
6737
6738fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
6739    // TODO: Don't duplicate this from lib.rs Program.
6740    ast.recast_top(&Default::default(), 0)
6741}
6742
6743struct FindNumericLiteral {
6744    target: SourceRange,
6745    found: Cell<Option<ast::NumericLiteral>>,
6746}
6747
6748impl<'a> crate::walk::Visitor<'a> for &FindNumericLiteral {
6749    type Error = crate::front::Error;
6750
6751    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6752        let Ok(node_range) = SourceRange::try_from(&node) else {
6753            return Ok(true);
6754        };
6755
6756        if node_range == self.target
6757            && let crate::walk::Node::NumericLiteral(literal) = node
6758        {
6759            self.found.set(Some(literal.inner.clone()));
6760            return Ok(false);
6761        }
6762
6763        for child in node.children().iter() {
6764            if !child.visit(*self)? {
6765                return Ok(false);
6766            }
6767        }
6768
6769        Ok(true)
6770    }
6771}
6772
6773fn numeric_literal_at_source_range(ast: &ast::Node<ast::Program>, target: SourceRange) -> Option<ast::NumericLiteral> {
6774    let find = FindNumericLiteral {
6775        target,
6776        found: Cell::new(None),
6777    };
6778    let node = crate::walk::Node::from(ast);
6779    node.visit(&find).ok()?;
6780    find.found.into_inner()
6781}
6782
6783struct FindSketchVarInitialByNodePath<'a> {
6784    target: &'a ast::NodePath,
6785    sketch_var_found: Cell<bool>,
6786    initial_literal: Cell<Option<ast::NumericLiteral>>,
6787}
6788
6789impl<'a, 'b> crate::walk::Visitor<'b> for &FindSketchVarInitialByNodePath<'a> {
6790    type Error = crate::front::Error;
6791
6792    fn visit_node(&self, node: crate::walk::Node<'b>) -> anyhow::Result<bool, Self::Error> {
6793        if let crate::walk::Node::SketchVar(sketch_var) = node
6794            && sketch_var.node_path.as_ref() == Some(self.target)
6795        {
6796            self.sketch_var_found.set(true);
6797            if let Some(initial) = &sketch_var.initial {
6798                self.initial_literal.set(Some(initial.inner.clone()));
6799            }
6800            return Ok(false);
6801        }
6802
6803        for child in node.children().iter() {
6804            if !child.visit(*self)? {
6805                return Ok(false);
6806            }
6807        }
6808
6809        Ok(true)
6810    }
6811}
6812
6813/// Locate the source `var` declaration corresponding to a sketch-var solution.
6814///
6815/// The outer [`Option`] distinguishes "no matching target" (commit must fail)
6816/// from "target found." The inner [`Option`] is the initial numeric literal of
6817/// the [`SketchVar`], if any; bare `var` declarations return `Some(None)`.
6818///
6819/// When `node_path` is `None` (e.g. for older outcomes that predate the
6820/// node-path propagation), this falls back to source-range matching, which
6821/// can break under whitespace shifts elsewhere in the file.
6822fn numeric_literal_at_node_path(
6823    ast: &ast::Node<ast::Program>,
6824    node_path: Option<&ast::NodePath>,
6825    source_range: SourceRange,
6826) -> Option<Option<ast::NumericLiteral>> {
6827    let Some(node_path) = node_path else {
6828        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";
6829        #[cfg(target_arch = "wasm32")]
6830        web_sys::console::warn_1(&message.into());
6831        #[cfg(not(target_arch = "wasm32"))]
6832        eprintln!("WARNING: {message}");
6833        return numeric_literal_at_source_range(ast, source_range).map(Some);
6834    };
6835    let find = FindSketchVarInitialByNodePath {
6836        target: node_path,
6837        sketch_var_found: Cell::new(false),
6838        initial_literal: Cell::new(None),
6839    };
6840    let node = crate::walk::Node::from(ast);
6841    node.visit(&find).ok()?;
6842    if !find.sketch_var_found.get() {
6843        return None;
6844    }
6845    Some(find.initial_literal.into_inner())
6846}
6847
6848fn suffix_length_unit(suffix: NumericSuffix) -> Option<UnitLength> {
6849    match suffix {
6850        NumericSuffix::Mm => Some(UnitLength::Millimeters),
6851        NumericSuffix::Cm => Some(UnitLength::Centimeters),
6852        NumericSuffix::M => Some(UnitLength::Meters),
6853        NumericSuffix::Inch => Some(UnitLength::Inches),
6854        NumericSuffix::Ft => Some(UnitLength::Feet),
6855        NumericSuffix::Yd => Some(UnitLength::Yards),
6856        _ => None,
6857    }
6858}
6859
6860fn number_value_in_default_length_units(number: Number, default_length_unit: UnitLength) -> f64 {
6861    match suffix_length_unit(number.units) {
6862        Some(unit) => adjust_length(unit, number.value, default_length_unit).0,
6863        None => number.value,
6864    }
6865}
6866
6867fn literal_value_in_default_length_units(literal: &ast::NumericLiteral, default_length_unit: UnitLength) -> f64 {
6868    match suffix_length_unit(literal.suffix) {
6869        Some(unit) => adjust_length(unit, literal.value, default_length_unit).0,
6870        None => literal.value,
6871    }
6872}
6873
6874fn var_solution_needs_commit(
6875    current_literal: &ast::NumericLiteral,
6876    solved_value: Number,
6877    default_length_unit: UnitLength,
6878) -> bool {
6879    let current = literal_value_in_default_length_units(current_literal, default_length_unit);
6880    let solved = number_value_in_default_length_units(solved_value, default_length_unit);
6881
6882    (current - solved).abs() > 1e-9
6883}
6884
6885fn preserve_var_solution_literal_style(
6886    current_literal: &ast::NumericLiteral,
6887    solved_value: Number,
6888    default_length_unit: UnitLength,
6889) -> Number {
6890    if current_literal.suffix == NumericSuffix::None {
6891        return Number {
6892            value: number_value_in_default_length_units(solved_value, default_length_unit),
6893            units: NumericSuffix::None,
6894        };
6895    }
6896
6897    let Some(current_unit) = suffix_length_unit(current_literal.suffix) else {
6898        return solved_value;
6899    };
6900
6901    let solved_default_value = number_value_in_default_length_units(solved_value, default_length_unit);
6902    Number {
6903        value: adjust_length(default_length_unit, solved_default_value, current_unit).0,
6904        units: current_literal.suffix,
6905    }
6906}
6907
6908pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
6909    Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node {
6910        inner: ast::ArrayExpression {
6911            elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
6912            non_code_meta: Default::default(),
6913            digest: None,
6914        },
6915        start: Default::default(),
6916        end: Default::default(),
6917        module_id: Default::default(),
6918        node_path: None,
6919        outer_attrs: Default::default(),
6920        pre_comments: Default::default(),
6921        comment_start: Default::default(),
6922    })))
6923}
6924
6925pub(crate) fn to_ast_point2d_array(points: &[Point2d<Expr>]) -> anyhow::Result<ast::Expr> {
6926    Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
6927        ast::ArrayExpression {
6928            elements: points.iter().map(to_ast_point2d).collect::<anyhow::Result<Vec<_>>>()?,
6929            digest: None,
6930            non_code_meta: Default::default(),
6931        },
6932    ))))
6933}
6934
6935fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
6936    Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
6937        ast::ArrayExpression {
6938            elements: vec![
6939                ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6940                    point.x,
6941                )?)))),
6942                ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6943                    point.y,
6944                )?)))),
6945            ],
6946            non_code_meta: Default::default(),
6947            digest: None,
6948        },
6949    ))))
6950}
6951
6952fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
6953    match expr {
6954        Expr::Number(number) => Ok(ast::Expr::Literal(BoxNode::new(ast::Node {
6955            inner: ast::Literal::from(to_source_number(*number)?),
6956            start: Default::default(),
6957            end: Default::default(),
6958            module_id: Default::default(),
6959            node_path: None,
6960            outer_attrs: Default::default(),
6961            pre_comments: Default::default(),
6962            comment_start: Default::default(),
6963        }))),
6964        Expr::Var(number) => Ok(ast::Expr::SketchVar(BoxNode::new(ast::Node {
6965            inner: ast::SketchVar {
6966                initial: Some(BoxNode::new(ast::Node {
6967                    inner: to_source_number(*number)?,
6968                    start: Default::default(),
6969                    end: Default::default(),
6970                    module_id: Default::default(),
6971                    node_path: None,
6972                    outer_attrs: Default::default(),
6973                    pre_comments: Default::default(),
6974                    comment_start: Default::default(),
6975                })),
6976                digest: None,
6977            },
6978            start: Default::default(),
6979            end: Default::default(),
6980            module_id: Default::default(),
6981            node_path: None,
6982            outer_attrs: Default::default(),
6983            pre_comments: Default::default(),
6984            comment_start: Default::default(),
6985        }))),
6986        Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
6987    }
6988}
6989
6990fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
6991    Ok(ast::NumericLiteral {
6992        value: number.value,
6993        suffix: number.units,
6994        raw: format_number_literal(number.value, number.units, None)?,
6995        digest: None,
6996    })
6997}
6998
6999pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
7000    ast::Expr::Name(BoxNode::new(ast_name(name)))
7001}
7002
7003fn ast_name(name: String) -> ast::Node<ast::Name> {
7004    ast::Node {
7005        inner: ast::Name {
7006            name: ast::Node {
7007                inner: ast::Identifier { name, digest: None },
7008                start: Default::default(),
7009                end: Default::default(),
7010                module_id: Default::default(),
7011                node_path: None,
7012                outer_attrs: Default::default(),
7013                pre_comments: Default::default(),
7014                comment_start: Default::default(),
7015            },
7016            path: Vec::new(),
7017            abs_path: false,
7018            digest: None,
7019        },
7020        start: Default::default(),
7021        end: Default::default(),
7022        module_id: Default::default(),
7023        node_path: None,
7024        outer_attrs: Default::default(),
7025        pre_comments: Default::default(),
7026        comment_start: Default::default(),
7027    }
7028}
7029
7030pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
7031    ast::Name {
7032        name: ast::Node {
7033            inner: ast::Identifier {
7034                name: name.to_owned(),
7035                digest: None,
7036            },
7037            start: Default::default(),
7038            end: Default::default(),
7039            module_id: Default::default(),
7040            node_path: None,
7041            outer_attrs: Default::default(),
7042            pre_comments: Default::default(),
7043            comment_start: Default::default(),
7044        },
7045        path: Default::default(),
7046        abs_path: false,
7047        digest: None,
7048    }
7049}
7050
7051// Shared AST creation helpers used by both frontend and transpiler to ensure consistency.
7052
7053/// Create an AST node for coincident([expr1, expr2, ...])
7054pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
7055    let elements = exprs.into_iter().collect::<Vec<_>>();
7056    debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
7057
7058    // Create array [expr1, expr2, ...]
7059    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7060        elements,
7061        digest: None,
7062        non_code_meta: Default::default(),
7063    })));
7064
7065    // Create coincident([...])
7066    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7067        callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
7068        unlabeled: Some(array_expr),
7069        arguments: Default::default(),
7070        digest: None,
7071        non_code_meta: Default::default(),
7072    })))
7073}
7074
7075/// Create an AST node for line(start = [...], end = [...])
7076pub(crate) fn create_line_ast(start_ast: ast::Expr, end_ast: ast::Expr) -> ast::Expr {
7077    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7078        callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
7079        unlabeled: None,
7080        arguments: vec![
7081            ast::LabeledArg {
7082                label: Some(ast::Identifier::new(LINE_START_PARAM)),
7083                arg: start_ast,
7084            },
7085            ast::LabeledArg {
7086                label: Some(ast::Identifier::new(LINE_END_PARAM)),
7087                arg: end_ast,
7088            },
7089        ],
7090        digest: None,
7091        non_code_meta: Default::default(),
7092    })))
7093}
7094
7095/// Create an AST node for arc(start = [...], end = [...], center = [...])
7096pub(crate) fn create_arc_ast(start_ast: ast::Expr, end_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
7097    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7098        callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
7099        unlabeled: None,
7100        arguments: vec![
7101            ast::LabeledArg {
7102                label: Some(ast::Identifier::new(ARC_START_PARAM)),
7103                arg: start_ast,
7104            },
7105            ast::LabeledArg {
7106                label: Some(ast::Identifier::new(ARC_END_PARAM)),
7107                arg: end_ast,
7108            },
7109            ast::LabeledArg {
7110                label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
7111                arg: center_ast,
7112            },
7113        ],
7114        digest: None,
7115        non_code_meta: Default::default(),
7116    })))
7117}
7118
7119/// Create an AST node for circle(start = [...], center = [...])
7120pub(crate) fn create_circle_ast(start_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
7121    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7122        callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
7123        unlabeled: None,
7124        arguments: vec![
7125            ast::LabeledArg {
7126                label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
7127                arg: start_ast,
7128            },
7129            ast::LabeledArg {
7130                label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
7131                arg: center_ast,
7132            },
7133        ],
7134        digest: None,
7135        non_code_meta: Default::default(),
7136    })))
7137}
7138
7139/// Create an AST node for horizontal(line)
7140pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
7141    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7142        callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
7143        unlabeled: Some(line_expr),
7144        arguments: Default::default(),
7145        digest: None,
7146        non_code_meta: Default::default(),
7147    })))
7148}
7149
7150/// Create an AST node for vertical(line)
7151pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
7152    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7153        callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
7154        unlabeled: Some(line_expr),
7155        arguments: Default::default(),
7156        digest: None,
7157        non_code_meta: Default::default(),
7158    })))
7159}
7160
7161/// Create a member expression like object.property (e.g., line1.end)
7162pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
7163    ast::Expr::MemberExpression(BoxNode::new(ast::Node::no_src(ast::MemberExpression {
7164        object: object_expr,
7165        property: ast::Expr::Name(BoxNode::new(ast::Node::no_src(ast::Name {
7166            name: ast::Node::no_src(ast::Identifier {
7167                name: property.to_string(),
7168                digest: None,
7169            }),
7170            path: Vec::new(),
7171            abs_path: false,
7172            digest: None,
7173        }))),
7174        computed: false,
7175        digest: None,
7176    })))
7177}
7178
7179pub(crate) fn create_index_expression(object_expr: ast::Expr, index: usize) -> ast::Expr {
7180    ast::Expr::MemberExpression(BoxNode::new(ast::Node::no_src(ast::MemberExpression {
7181        object: object_expr,
7182        property: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(
7183            ast::NumericLiteral {
7184                value: index as f64,
7185                suffix: NumericSuffix::None,
7186                raw: index.to_string(),
7187                digest: None,
7188            },
7189        )))),
7190        computed: true,
7191        digest: None,
7192    })))
7193}
7194
7195/// Create an AST node for `fixed([point, [x, y]])`.
7196fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
7197    // Create [x, y] array literal.
7198    let x_literal = ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
7199        position.x,
7200    )?))));
7201    let y_literal = ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
7202        position.y,
7203    )?))));
7204    let point_array = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7205        elements: vec![x_literal, y_literal],
7206        digest: None,
7207        non_code_meta: Default::default(),
7208    })));
7209
7210    // Create [point, [x, y]] outer array.
7211    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7212        elements: vec![point_expr, point_array],
7213        digest: None,
7214        non_code_meta: Default::default(),
7215    })));
7216
7217    // Create fixed([...])
7218    Ok(ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(
7219        ast::CallExpressionKw {
7220            callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
7221            unlabeled: Some(array_expr),
7222            arguments: Default::default(),
7223            digest: None,
7224            non_code_meta: Default::default(),
7225        },
7226    ))))
7227}
7228
7229/// Create an AST node for equalLength([line1, line2, ...])
7230pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
7231    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7232        elements: line_exprs,
7233        digest: None,
7234        non_code_meta: Default::default(),
7235    })));
7236
7237    // Create equalLength([...])
7238    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7239        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
7240        unlabeled: Some(array_expr),
7241        arguments: Default::default(),
7242        digest: None,
7243        non_code_meta: Default::default(),
7244    })))
7245}
7246
7247/// Create an AST node for equalRadius([seg1, seg2, ...])
7248pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
7249    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7250        elements: segment_exprs,
7251        digest: None,
7252        non_code_meta: Default::default(),
7253    })));
7254
7255    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7256        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
7257        unlabeled: Some(array_expr),
7258        arguments: Default::default(),
7259        digest: None,
7260        non_code_meta: Default::default(),
7261    })))
7262}
7263
7264/// Create an AST node for tangent([seg1, seg2])
7265pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
7266    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7267        elements: vec![seg1_expr, seg2_expr],
7268        digest: None,
7269        non_code_meta: Default::default(),
7270    })));
7271
7272    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7273        callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
7274        unlabeled: Some(array_expr),
7275        arguments: Default::default(),
7276        digest: None,
7277        non_code_meta: Default::default(),
7278    })))
7279}
7280
7281/// Create an AST node for symmetric([input1, input2], axis = line)
7282pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
7283    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7284        elements: input_exprs,
7285        digest: None,
7286        non_code_meta: Default::default(),
7287    })));
7288    let arguments = vec![ast::LabeledArg {
7289        label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
7290        arg: axis_expr,
7291    }];
7292
7293    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7294        callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
7295        unlabeled: Some(array_expr),
7296        arguments,
7297        digest: None,
7298        non_code_meta: Default::default(),
7299    })))
7300}
7301
7302/// Create an AST node for midpoint(segment, point = point)
7303pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
7304    let arguments = vec![ast::LabeledArg {
7305        label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
7306        arg: point_expr,
7307    }];
7308
7309    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7310        callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
7311        unlabeled: Some(segment_expr),
7312        arguments,
7313        digest: None,
7314        non_code_meta: Default::default(),
7315    })))
7316}
7317
7318/// The source range to attach to a diagnostic for `error` in the top-level
7319/// module. Source ranges are ordered innermost first and may point into
7320/// imported modules, so prefer the innermost range that is in the top-level
7321/// module; otherwise fall back to the innermost range.
7322fn issue_source_range(error: &KclError) -> SourceRange {
7323    let source_ranges = error.source_ranges();
7324    source_ranges
7325        .iter()
7326        .find(|range| range.is_top_level_module())
7327        .or_else(|| source_ranges.first())
7328        .copied()
7329        .unwrap_or_else(SourceRange::synthetic)
7330}
7331
7332#[cfg(test)]
7333mod tests {
7334    use std::sync;
7335
7336    use super::*;
7337    use crate::engine::PlaneName;
7338    use crate::engine::engine_manager::EngineManager;
7339    use crate::execution::cache::SketchModeState;
7340    use crate::execution::cache::clear_mem_cache;
7341    use crate::execution::cache::read_old_memory;
7342    use crate::execution::cache::write_old_memory;
7343    use crate::front::Distance;
7344    use crate::front::Fixed;
7345    use crate::front::FixedPoint;
7346    use crate::front::Midpoint;
7347    use crate::front::Object;
7348    use crate::front::Plane;
7349    use crate::front::Sketch;
7350    use crate::front::Tangent;
7351    use crate::frontend::sketch::Vertical;
7352    use crate::pretty::NumericSuffix;
7353
7354    fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
7355        for object in &scene_graph.objects {
7356            if let ObjectKind::Sketch(_) = &object.kind {
7357                return Some(object);
7358            }
7359        }
7360        None
7361    }
7362
7363    fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
7364        for object in &scene_graph.objects {
7365            if let ObjectKind::Face(_) = &object.kind {
7366                return Some(object);
7367            }
7368        }
7369        None
7370    }
7371
7372    fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
7373        for object in &scene_graph.objects {
7374            if matches!(&object.kind, ObjectKind::Wall(_)) {
7375                return Some(object.id);
7376            }
7377        }
7378        None
7379    }
7380
7381    fn find_cap_object_id_with_solid_output_index(
7382        scene_graph: &SceneGraph,
7383        cap_kind: crate::frontend::api::CapKind,
7384        solid_output_index: usize,
7385    ) -> Option<ObjectId> {
7386        for object in &scene_graph.objects {
7387            if matches!(&object.kind, ObjectKind::Cap(cap) if cap.kind == cap_kind && cap.solid_output_index == Some(solid_output_index))
7388            {
7389                return Some(object.id);
7390            }
7391        }
7392        None
7393    }
7394
7395    #[test]
7396    fn issue_source_range_prefers_top_level_module() {
7397        use kcl_error::ModuleId;
7398
7399        let top = SourceRange::new(10, 20, ModuleId::default());
7400        let imported = SourceRange::new(0, 5, ModuleId::from_usize(7));
7401
7402        // Innermost frame is in an imported module; the diagnostic should
7403        // land on the innermost top-level range instead.
7404        let error = KclError::new_semantic(crate::errors::KclErrorDetails::new(
7405            "boom".to_owned(),
7406            vec![imported, top],
7407        ));
7408        assert_eq!(super::issue_source_range(&error), top);
7409
7410        // No top-level range at all: fall back to the innermost one.
7411        let error = KclError::new_semantic(crate::errors::KclErrorDetails::new("boom".to_owned(), vec![imported]));
7412        assert_eq!(super::issue_source_range(&error), imported);
7413
7414        // No ranges: synthetic.
7415        let error = KclError::new_semantic(crate::errors::KclErrorDetails::new("boom".to_owned(), vec![]));
7416        assert_eq!(super::issue_source_range(&error), SourceRange::synthetic());
7417    }
7418
7419    #[test]
7420    fn composite_constituent_sweeps_are_not_solid_outputs() {
7421        use kcl_api::artifact::ArtifactSweepMethod;
7422        use kcl_api::artifact::CompositeSolid;
7423        use kcl_api::artifact::CompositeSolidSubType;
7424        use kcl_api::artifact::Sweep;
7425        use kcl_api::artifact::SweepSubType;
7426
7427        let first_sweep_id = ArtifactId::new(Uuid::new_v4());
7428        let second_sweep_id = ArtifactId::new(Uuid::new_v4());
7429        let composite_id = ArtifactId::new(Uuid::new_v4());
7430        let code_ref = CodeRef::placeholder(SourceRange::synthetic());
7431        let sweep = |id| {
7432            Artifact::Sweep(Sweep {
7433                id,
7434                sub_type: SweepSubType::Extrusion,
7435                path_id: ArtifactId::new(Uuid::new_v4()),
7436                surface_ids: Vec::new(),
7437                edge_ids: Vec::new(),
7438                code_ref: code_ref.clone(),
7439                source_sweep_id: None,
7440                trajectory_id: None,
7441                method: ArtifactSweepMethod::New,
7442                consumed: false,
7443                pattern_ids: Vec::new(),
7444            })
7445        };
7446        let mut artifacts = IndexMap::from([
7447            (first_sweep_id, sweep(first_sweep_id)),
7448            (second_sweep_id, sweep(second_sweep_id)),
7449        ]);
7450
7451        let top_level_graph = ArtifactGraph::from_parts(artifacts.clone(), artifacts.len());
7452        assert_eq!(
7453            solid_output_index_for_sweep(&top_level_graph, first_sweep_id, &code_ref),
7454            Some(0)
7455        );
7456        assert_eq!(
7457            solid_output_index_for_sweep(&top_level_graph, second_sweep_id, &code_ref),
7458            Some(1)
7459        );
7460
7461        artifacts.insert(
7462            composite_id,
7463            Artifact::CompositeSolid(CompositeSolid {
7464                id: composite_id,
7465                consumed: false,
7466                sub_type: CompositeSolidSubType::Union,
7467                output_index: None,
7468                solid_ids: vec![first_sweep_id, second_sweep_id],
7469                tool_ids: Vec::new(),
7470                code_ref,
7471                composite_solid_id: None,
7472                pattern_ids: Vec::new(),
7473            }),
7474        );
7475        let composite_graph = ArtifactGraph::from_parts(artifacts.clone(), artifacts.len());
7476        assert_eq!(
7477            solid_output_index_for_sweep(&composite_graph, first_sweep_id, &CodeRef::default()),
7478            None
7479        );
7480        assert_eq!(
7481            solid_output_index_for_sweep(&composite_graph, second_sweep_id, &CodeRef::default()),
7482            None
7483        );
7484    }
7485
7486    #[test]
7487    fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
7488        let source = "\
7489region001 = region(point = [0.1, 0.1], sketch = s)
7490extrude001 = extrude(region001, length = 5)
7491revolve001 = revolve(region001, axis = Y)
7492sweep001 = sweep(region001, path = path001)
7493loft001 = loft(region001)
7494not_sweep001 = shell(extrude001, faces = [], thickness = 1)
7495";
7496
7497        let program = Program::parse(source).unwrap().0.unwrap();
7498
7499        assert_eq!(
7500            region_name_from_sweep_variable(&program.ast, "extrude001"),
7501            Some("region001".to_owned())
7502        );
7503        assert_eq!(
7504            region_name_from_sweep_variable(&program.ast, "revolve001"),
7505            Some("region001".to_owned())
7506        );
7507        assert_eq!(
7508            region_name_from_sweep_variable(&program.ast, "sweep001"),
7509            Some("region001".to_owned())
7510        );
7511        assert_eq!(
7512            region_name_from_sweep_variable(&program.ast, "loft001"),
7513            Some("region001".to_owned())
7514        );
7515        assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
7516    }
7517
7518    #[track_caller]
7519    fn expect_sketch(object: &Object) -> &Sketch {
7520        if let ObjectKind::Sketch(sketch) = &object.kind {
7521            sketch
7522        } else {
7523            panic!("Object is not a sketch: {:?}", object);
7524        }
7525    }
7526
7527    fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
7528        let point_object = scene_graph.objects.get(point_id.0).unwrap();
7529        let ObjectKind::Segment {
7530            segment: Segment::Point(point),
7531        } = &point_object.kind
7532        else {
7533            panic!("Object is not a point segment: {point_object:?}");
7534        };
7535        point.position.clone()
7536    }
7537
7538    fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
7539        assert!((actual.x.value - expected.x.value).abs() < 1e-6);
7540        assert!((actual.y.value - expected.y.value).abs() < 1e-6);
7541    }
7542
7543    /// Build a millimeter-valued point expression for concise sketch edit test
7544    /// setup.
7545    fn point_expr_mm(x: f64, y: f64) -> Point2d<Expr> {
7546        Point2d {
7547            x: Expr::Var(Number {
7548                value: x,
7549                units: NumericSuffix::Mm,
7550            }),
7551            y: Expr::Var(Number {
7552                value: y,
7553                units: NumericSuffix::Mm,
7554            }),
7555        }
7556    }
7557
7558    /// Build a millimeter-valued numeric point for comparing solved scene graph
7559    /// positions.
7560    fn point_number_mm(x: f64, y: f64) -> Point2d<Number> {
7561        Point2d {
7562            x: Number {
7563                value: x,
7564                units: NumericSuffix::Mm,
7565            },
7566            y: Number {
7567                value: y,
7568                units: NumericSuffix::Mm,
7569            },
7570        }
7571    }
7572
7573    fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
7574        LineCtor {
7575            start: Point2d {
7576                x: Expr::Number(Number { value: start_x, units }),
7577                y: Expr::Number(Number { value: start_y, units }),
7578            },
7579            end: Point2d {
7580                x: Expr::Number(Number { value: end_x, units }),
7581                y: Expr::Number(Number { value: end_y, units }),
7582            },
7583            construction: None,
7584        }
7585    }
7586
7587    async fn create_sketch_with_single_line(
7588        frontend: &mut FrontendState,
7589        ctx: &ExecutorContext,
7590        mock_ctx: &ExecutorContext,
7591        version: Version,
7592    ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
7593        frontend.program = Program::empty();
7594
7595        let sketch_args = SketchCtor {
7596            on: Plane::Default(PlaneName::Xy),
7597        };
7598        let (_src_delta, _scene_delta, sketch_id) = frontend
7599            .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
7600            .await
7601            .unwrap();
7602
7603        let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
7604        let (source_delta, scene_graph_delta) = frontend
7605            .add_segment(mock_ctx, version, sketch_id, segment, None)
7606            .await
7607            .unwrap();
7608        let line_id = *scene_graph_delta
7609            .new_objects
7610            .last()
7611            .expect("Expected line object id to be created");
7612
7613        (sketch_id, line_id, source_delta, scene_graph_delta)
7614    }
7615
7616    async fn seed_frontend_with_mock(frontend: &mut FrontendState, mock_ctx: &ExecutorContext, program: &Program) {
7617        frontend.program = program.clone();
7618        let outcome = mock_ctx.run_mock(program, &MockConfig::default()).await.unwrap();
7619        frontend.update_state_after_exec(outcome, true);
7620    }
7621
7622    #[test]
7623    fn test_parse_frontend_mutation_source_error_messages_are_user_facing() {
7624        for (source, expected_message) in [
7625            ("**", "Error parsing KCL source after editing: Unexpected token: *"),
7626            ("3'", "Error parsing KCL source after editing: found unknown token '''"),
7627        ] {
7628            let err = parse_frontend_mutation_source(
7629                source,
7630                "Error parsing KCL source after editing",
7631                "No AST produced after editing",
7632            )
7633            .expect_err("expected invalid KCL source to fail");
7634            let message = err.error.message();
7635
7636            assert_eq!(message, expected_message);
7637            assert!(!message.contains("CompilationIssue"));
7638            assert!(!message.contains("KclErrorDetails"));
7639            assert!(!message.contains("source_range"));
7640        }
7641    }
7642
7643    #[tokio::test(flavor = "multi_thread")]
7644    async fn test_edit_constraint_value_parse_error_messages_are_user_facing() {
7645        let initial_source = "\
7646sketch(on = XY) {
7647  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7648  distance([line1.start, line1.end]) == 10
7649}
7650";
7651        let program = Program::parse(initial_source).unwrap().0.unwrap();
7652
7653        let mut frontend = FrontendState::new();
7654        let mock_ctx = ExecutorContext::new_mock(None).await;
7655        let version = Version(0);
7656
7657        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7658        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7659        let sketch_id = sketch_object.id;
7660        let sketch = expect_sketch(sketch_object);
7661        let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7662
7663        for (value, expected_message) in [
7664            ("**", "Invalid constraint value: Unexpected token: *"),
7665            ("3'", "Invalid constraint value: found unknown token '''"),
7666        ] {
7667            let err = frontend
7668                .edit_constraint_value(&mock_ctx, version, sketch_id, constraint_id, value.to_owned())
7669                .await
7670                .expect_err("expected invalid constraint expression to fail");
7671            let message = err.error.message();
7672
7673            assert_eq!(message, expected_message);
7674            assert!(!message.contains("CompilationIssue"));
7675            assert!(!message.contains("KclErrorDetails"));
7676            assert!(!message.contains("source_range"));
7677        }
7678
7679        mock_ctx.close().await;
7680    }
7681
7682    #[tokio::test(flavor = "multi_thread")]
7683    async fn test_failed_edit_constraint_value_does_not_update_program() {
7684        let initial_source = "\
7685sketch(on = XY) {
7686  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7687  distance([line1.start, line1.end]) == 10
7688}
7689";
7690        let program = Program::parse(initial_source).unwrap().0.unwrap();
7691        let original_source = program.original_file_contents.clone();
7692
7693        let mut frontend = FrontendState::new();
7694        let mock_ctx = ExecutorContext::new_mock(None).await;
7695        let version = Version(0);
7696
7697        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7698        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7699        let sketch_id = sketch_object.id;
7700        let sketch = expect_sketch(sketch_object);
7701        let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7702
7703        frontend
7704            .edit_constraint_value(
7705                &mock_ctx,
7706                version,
7707                sketch_id,
7708                constraint_id,
7709                "unknownDistance".to_owned(),
7710            )
7711            .await
7712            .expect_err("expected invalid constraint value to fail execution");
7713
7714        assert_eq!(frontend.program.original_file_contents, original_source);
7715        assert!(!frontend.program.original_file_contents.contains("unknownDistance"));
7716
7717        mock_ctx.close().await;
7718    }
7719
7720    #[tokio::test(flavor = "multi_thread")]
7721    async fn test_edit_constraint_value_array_index_oob_fails_in_sketch_mode() {
7722        let initial_source = "\
7723arr = [0]
7724sketch(on = XY) {
7725  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7726  distance([line1.start, line1.end]) == 10
7727}
7728";
7729        let program = Program::parse(initial_source).unwrap().0.unwrap();
7730
7731        let mut frontend = FrontendState::new();
7732        let mock_ctx = ExecutorContext::new_mock(None).await;
7733        let version = Version(0);
7734
7735        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7736        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7737        let sketch_id = sketch_object.id;
7738        let sketch = expect_sketch(sketch_object);
7739        let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7740
7741        // In sketch mode execution, an out-of-bounds array index is an error.
7742        // It should not fall back to the first element of the array the way
7743        // plain mock execution does.
7744        let err = frontend
7745            .edit_constraint_value(&mock_ctx, version, sketch_id, constraint_id, "arr[5]".to_owned())
7746            .await
7747            .expect_err("expected out-of-bounds array index to be an error in sketch mode execution");
7748        let message = err.error.message();
7749        assert!(
7750            message.contains("The array doesn't have any item at index 5"),
7751            "unexpected error message: {message}"
7752        );
7753
7754        mock_ctx.close().await;
7755    }
7756
7757    #[tokio::test(flavor = "multi_thread")]
7758    async fn test_sketch_checkpoint_round_trip_restores_state() {
7759        let mut frontend = FrontendState::new();
7760        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7761        let mock_ctx = ExecutorContext::new_mock(None).await;
7762        let version = Version(0);
7763
7764        let (sketch_id, line_id, source_delta, scene_graph_delta) =
7765            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7766
7767        let expected_source = source_delta.text.clone();
7768        let expected_scene_graph = frontend.scene_graph.clone();
7769        let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
7770        let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
7771
7772        let checkpoint_id = frontend
7773            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7774            .await
7775            .unwrap();
7776
7777        let edited_segments = vec![ExistingSegmentCtor {
7778            id: line_id,
7779            ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
7780        }];
7781        let (edited_source, _edited_scene) = frontend
7782            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
7783            .await
7784            .unwrap();
7785        assert_ne!(edited_source.text, expected_source);
7786
7787        let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
7788
7789        assert_eq!(restored.source_delta.text, expected_source);
7790        assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
7791        assert!(restored.scene_graph_delta.invalidates_ids);
7792        assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
7793        assert_eq!(frontend.scene_graph, expected_scene_graph);
7794        assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
7795
7796        ctx.close().await;
7797    }
7798
7799    #[tokio::test(flavor = "multi_thread")]
7800    async fn test_sketch_checkpoints_prune_oldest_entries() {
7801        let mut frontend = FrontendState::new();
7802        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7803        let mock_ctx = ExecutorContext::new_mock(None).await;
7804        let version = Version(0);
7805
7806        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7807            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7808
7809        let mut checkpoint_ids = Vec::new();
7810        for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
7811            checkpoint_ids.push(
7812                frontend
7813                    .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7814                    .await
7815                    .unwrap(),
7816            );
7817        }
7818
7819        assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
7820        assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
7821
7822        let oldest_retained = checkpoint_ids[3];
7823        assert_eq!(
7824            frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
7825            Some(oldest_retained)
7826        );
7827
7828        let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
7829        assert!(evicted_restore.is_err());
7830        assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
7831
7832        frontend
7833            .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
7834            .await
7835            .unwrap();
7836
7837        ctx.close().await;
7838    }
7839
7840    #[tokio::test(flavor = "multi_thread")]
7841    async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
7842        let mut frontend = FrontendState::new();
7843        let missing_checkpoint = SketchCheckpointId::new(999);
7844
7845        let err = frontend
7846            .restore_sketch_checkpoint(missing_checkpoint)
7847            .await
7848            .expect_err("Expected restore to fail for missing checkpoint");
7849
7850        assert!(err.msg.contains("Sketch checkpoint not found"));
7851    }
7852
7853    #[tokio::test(flavor = "multi_thread")]
7854    async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
7855        let mut frontend = FrontendState::new();
7856        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7857        let mock_ctx = ExecutorContext::new_mock(None).await;
7858        let version = Version(0);
7859
7860        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7861            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7862
7863        let checkpoint_a = frontend
7864            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7865            .await
7866            .unwrap();
7867        let checkpoint_b = frontend
7868            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7869            .await
7870            .unwrap();
7871        assert_eq!(frontend.sketch_checkpoints.len(), 2);
7872
7873        frontend.clear_sketch_checkpoints();
7874        assert!(frontend.sketch_checkpoints.is_empty());
7875        frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
7876        frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
7877
7878        ctx.close().await;
7879    }
7880
7881    #[tokio::test(flavor = "multi_thread")]
7882    async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
7883        let mut frontend = FrontendState::new();
7884        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7885        let mock_ctx = ExecutorContext::new_mock(None).await;
7886        let version = Version(0);
7887
7888        let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
7889            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7890        let old_source = source_delta.text.clone();
7891        let old_checkpoint = frontend
7892            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7893            .await
7894            .unwrap();
7895        let initial_checkpoint_count = frontend.sketch_checkpoints.len();
7896
7897        let new_program = Program::parse("sketch(on = XY) {\n  point(at = [1mm, 2mm])\n}\n")
7898            .unwrap()
7899            .0
7900            .unwrap();
7901
7902        let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
7903        let SetProgramOutcome::Success {
7904            checkpoint_id: Some(new_checkpoint),
7905            ..
7906        } = result
7907        else {
7908            panic!("Expected Success with a fresh checkpoint baseline");
7909        };
7910
7911        assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
7912
7913        let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7914        assert_eq!(old_restore.source_delta.text, old_source);
7915
7916        let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
7917        assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
7918
7919        ctx.close().await;
7920    }
7921
7922    #[tokio::test(flavor = "multi_thread")]
7923    async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
7924        let mut frontend = FrontendState::new();
7925        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7926        let mock_ctx = ExecutorContext::new_mock(None).await;
7927        let version = Version(0);
7928
7929        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7930            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7931        let old_checkpoint = frontend
7932            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7933            .await
7934            .unwrap();
7935        let checkpoint_count_before = frontend.sketch_checkpoints.len();
7936
7937        let failing_program = Program::parse(
7938            "sketch(on = XY) {\n  line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
7939        )
7940        .unwrap()
7941        .0
7942        .unwrap();
7943
7944        let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
7945        assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
7946        assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
7947        frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7948
7949        ctx.close().await;
7950    }
7951
7952    #[tokio::test(flavor = "multi_thread")]
7953    async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
7954        let mut frontend = FrontendState::new();
7955        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7956
7957        let program = Program::parse(
7958            "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",
7959        )
7960        .unwrap()
7961        .0
7962        .unwrap();
7963        let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
7964        let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
7965            panic!("Expected successful baseline program execution");
7966        };
7967
7968        clear_mem_cache().await;
7969        assert!(read_old_memory().await.is_none());
7970
7971        let checkpoint_without_mock_memory = frontend
7972            .create_sketch_checkpoint((*exec_outcome).clone())
7973            .await
7974            .unwrap();
7975
7976        write_old_memory(SketchModeState::new_for_tests()).await;
7977        assert!(read_old_memory().await.is_some());
7978
7979        let checkpoint_with_mock_memory = frontend
7980            .create_sketch_checkpoint((*exec_outcome).clone())
7981            .await
7982            .unwrap();
7983
7984        clear_mem_cache().await;
7985        assert!(read_old_memory().await.is_none());
7986
7987        frontend
7988            .restore_sketch_checkpoint(checkpoint_with_mock_memory)
7989            .await
7990            .unwrap();
7991        assert!(read_old_memory().await.is_some());
7992
7993        frontend
7994            .restore_sketch_checkpoint(checkpoint_without_mock_memory)
7995            .await
7996            .unwrap();
7997        assert!(read_old_memory().await.is_none());
7998
7999        ctx.close().await;
8000    }
8001
8002    #[tokio::test(flavor = "multi_thread")]
8003    async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
8004        let source = "\
8005sketch(on = XY) {
8006  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
8007}
8008
8009bad = missing_name
8010";
8011        let program = Program::parse(source).unwrap().0.unwrap();
8012
8013        let mut frontend = FrontendState::new();
8014
8015        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8016        let mock_ctx = ExecutorContext::new_mock(None).await;
8017        let version = Version(0);
8018        let project_id = ProjectId(0);
8019        let file_id = FileId(0);
8020
8021        let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
8022            panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
8023        };
8024
8025        let sketch_id = frontend
8026            .scene_graph
8027            .objects
8028            .iter()
8029            .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
8030            .expect("Expected sketch object from errored hack_set_program");
8031
8032        frontend
8033            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
8034            .await
8035            .unwrap();
8036
8037        ctx.close().await;
8038        mock_ctx.close().await;
8039    }
8040
8041    #[tokio::test(flavor = "multi_thread")]
8042    async fn test_new_sketch_add_point_edit_point() {
8043        let program = Program::empty();
8044
8045        let mut frontend = FrontendState::new();
8046        frontend.program = program;
8047
8048        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8049        let mock_ctx = ExecutorContext::new_mock(None).await;
8050        let version = Version(0);
8051
8052        let sketch_args = SketchCtor {
8053            on: Plane::Default(PlaneName::Xy),
8054        };
8055        let (_src_delta, scene_delta, sketch_id) = frontend
8056            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8057            .await
8058            .unwrap();
8059        assert_eq!(sketch_id, ObjectId(1));
8060        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8061        let sketch_object = &scene_delta.new_graph.objects[1];
8062        assert_eq!(sketch_object.id, ObjectId(1));
8063        assert_eq!(
8064            sketch_object.kind,
8065            ObjectKind::Sketch(Sketch {
8066                args: SketchCtor {
8067                    on: Plane::Default(PlaneName::Xy)
8068                },
8069                plane: ObjectId(0),
8070                segments: vec![],
8071                constraints: vec![],
8072            })
8073        );
8074        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8075
8076        let point_ctor = PointCtor {
8077            position: Point2d {
8078                x: Expr::Number(Number {
8079                    value: 1.0,
8080                    units: NumericSuffix::Inch,
8081                }),
8082                y: Expr::Number(Number {
8083                    value: 2.0,
8084                    units: NumericSuffix::Inch,
8085                }),
8086            },
8087        };
8088        let segment = SegmentCtor::Point(point_ctor);
8089        let (src_delta, scene_delta) = frontend
8090            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8091            .await
8092            .unwrap();
8093        insta::assert_snapshot!("test_new_sketch_add_point_edit_point_1", src_delta.text.as_str());
8094        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
8095        assert_eq!(scene_delta.new_graph.objects.len(), 3);
8096        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8097            assert_eq!(scene_object.id.0, i);
8098        }
8099
8100        let point_id = *scene_delta.new_objects.last().unwrap();
8101
8102        let point_ctor = PointCtor {
8103            position: Point2d {
8104                x: Expr::Number(Number {
8105                    value: 3.0,
8106                    units: NumericSuffix::Inch,
8107                }),
8108                y: Expr::Number(Number {
8109                    value: 4.0,
8110                    units: NumericSuffix::Inch,
8111                }),
8112            },
8113        };
8114        let segments = vec![ExistingSegmentCtor {
8115            id: point_id,
8116            ctor: SegmentCtor::Point(point_ctor),
8117        }];
8118        let (src_delta, scene_delta) = frontend
8119            .edit_segments(&mock_ctx, version, sketch_id, segments)
8120            .await
8121            .unwrap();
8122        insta::assert_snapshot!("test_new_sketch_add_point_edit_point_2", src_delta.text.as_str());
8123        assert_eq!(scene_delta.new_objects, vec![]);
8124        assert_eq!(scene_delta.new_graph.objects.len(), 3);
8125
8126        ctx.close().await;
8127        mock_ctx.close().await;
8128    }
8129
8130    #[tokio::test(flavor = "multi_thread")]
8131    async fn test_new_sketch_add_line_edit_line() {
8132        let program = Program::empty();
8133
8134        let mut frontend = FrontendState::new();
8135        frontend.program = program;
8136
8137        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8138        let mock_ctx = ExecutorContext::new_mock(None).await;
8139        let version = Version(0);
8140
8141        let sketch_args = SketchCtor {
8142            on: Plane::Default(PlaneName::Xy),
8143        };
8144        let (_src_delta, scene_delta, sketch_id) = frontend
8145            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8146            .await
8147            .unwrap();
8148        assert_eq!(sketch_id, ObjectId(1));
8149        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8150        let sketch_object = &scene_delta.new_graph.objects[1];
8151        assert_eq!(sketch_object.id, ObjectId(1));
8152        assert_eq!(
8153            sketch_object.kind,
8154            ObjectKind::Sketch(Sketch {
8155                args: SketchCtor {
8156                    on: Plane::Default(PlaneName::Xy)
8157                },
8158                plane: ObjectId(0),
8159                segments: vec![],
8160                constraints: vec![],
8161            })
8162        );
8163        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8164
8165        let line_ctor = LineCtor {
8166            start: Point2d {
8167                x: Expr::Number(Number {
8168                    value: 0.0,
8169                    units: NumericSuffix::Mm,
8170                }),
8171                y: Expr::Number(Number {
8172                    value: 0.0,
8173                    units: NumericSuffix::Mm,
8174                }),
8175            },
8176            end: Point2d {
8177                x: Expr::Number(Number {
8178                    value: 10.0,
8179                    units: NumericSuffix::Mm,
8180                }),
8181                y: Expr::Number(Number {
8182                    value: 10.0,
8183                    units: NumericSuffix::Mm,
8184                }),
8185            },
8186            construction: None,
8187        };
8188        let segment = SegmentCtor::Line(line_ctor);
8189        let (src_delta, scene_delta) = frontend
8190            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8191            .await
8192            .unwrap();
8193        insta::assert_snapshot!("test_new_sketch_add_line_edit_line_1", src_delta.text.as_str());
8194        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8195        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8196        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8197            assert_eq!(scene_object.id.0, i);
8198        }
8199
8200        // The new objects are the end points and then the line.
8201        let line = *scene_delta.new_objects.last().unwrap();
8202
8203        let line_ctor = LineCtor {
8204            start: Point2d {
8205                x: Expr::Number(Number {
8206                    value: 1.0,
8207                    units: NumericSuffix::Mm,
8208                }),
8209                y: Expr::Number(Number {
8210                    value: 2.0,
8211                    units: NumericSuffix::Mm,
8212                }),
8213            },
8214            end: Point2d {
8215                x: Expr::Number(Number {
8216                    value: 13.0,
8217                    units: NumericSuffix::Mm,
8218                }),
8219                y: Expr::Number(Number {
8220                    value: 14.0,
8221                    units: NumericSuffix::Mm,
8222                }),
8223            },
8224            construction: None,
8225        };
8226        let segments = vec![ExistingSegmentCtor {
8227            id: line,
8228            ctor: SegmentCtor::Line(line_ctor),
8229        }];
8230        let (src_delta, scene_delta) = frontend
8231            .edit_segments(&mock_ctx, version, sketch_id, segments)
8232            .await
8233            .unwrap();
8234        insta::assert_snapshot!("test_new_sketch_add_line_edit_line_2", src_delta.text.as_str());
8235        assert_eq!(scene_delta.new_objects, vec![]);
8236        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8237
8238        ctx.close().await;
8239        mock_ctx.close().await;
8240    }
8241
8242    #[tokio::test(flavor = "multi_thread")]
8243    async fn test_new_sketch_add_arc_edit_arc() {
8244        let program = Program::empty();
8245
8246        let mut frontend = FrontendState::new();
8247        frontend.program = program;
8248
8249        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8250        let mock_ctx = ExecutorContext::new_mock(None).await;
8251        let version = Version(0);
8252
8253        let sketch_args = SketchCtor {
8254            on: Plane::Default(PlaneName::Xy),
8255        };
8256        let (_src_delta, scene_delta, sketch_id) = frontend
8257            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8258            .await
8259            .unwrap();
8260        assert_eq!(sketch_id, ObjectId(1));
8261        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8262        let sketch_object = &scene_delta.new_graph.objects[1];
8263        assert_eq!(sketch_object.id, ObjectId(1));
8264        assert_eq!(
8265            sketch_object.kind,
8266            ObjectKind::Sketch(Sketch {
8267                args: SketchCtor {
8268                    on: Plane::Default(PlaneName::Xy),
8269                },
8270                plane: ObjectId(0),
8271                segments: vec![],
8272                constraints: vec![],
8273            })
8274        );
8275        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8276
8277        let arc_ctor = ArcCtor {
8278            start: Point2d {
8279                x: Expr::Var(Number {
8280                    value: 0.0,
8281                    units: NumericSuffix::Mm,
8282                }),
8283                y: Expr::Var(Number {
8284                    value: 0.0,
8285                    units: NumericSuffix::Mm,
8286                }),
8287            },
8288            end: Point2d {
8289                x: Expr::Var(Number {
8290                    value: 10.0,
8291                    units: NumericSuffix::Mm,
8292                }),
8293                y: Expr::Var(Number {
8294                    value: 10.0,
8295                    units: NumericSuffix::Mm,
8296                }),
8297            },
8298            center: Point2d {
8299                x: Expr::Var(Number {
8300                    value: 10.0,
8301                    units: NumericSuffix::Mm,
8302                }),
8303                y: Expr::Var(Number {
8304                    value: 0.0,
8305                    units: NumericSuffix::Mm,
8306                }),
8307            },
8308            direction: None,
8309            construction: None,
8310        };
8311        let segment = SegmentCtor::Arc(arc_ctor);
8312        let (src_delta, scene_delta) = frontend
8313            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8314            .await
8315            .unwrap();
8316        insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_1", src_delta.text.as_str());
8317        assert_eq!(
8318            scene_delta.new_objects,
8319            vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
8320        );
8321        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8322            assert_eq!(scene_object.id.0, i);
8323        }
8324        assert_eq!(scene_delta.new_graph.objects.len(), 6);
8325
8326        // The new objects are the end points, the center, and then the arc.
8327        let arc = *scene_delta.new_objects.last().unwrap();
8328
8329        let arc_ctor = ArcCtor {
8330            start: Point2d {
8331                x: Expr::Var(Number {
8332                    value: 1.0,
8333                    units: NumericSuffix::Mm,
8334                }),
8335                y: Expr::Var(Number {
8336                    value: 2.0,
8337                    units: NumericSuffix::Mm,
8338                }),
8339            },
8340            end: Point2d {
8341                x: Expr::Var(Number {
8342                    value: 13.0,
8343                    units: NumericSuffix::Mm,
8344                }),
8345                y: Expr::Var(Number {
8346                    value: 14.0,
8347                    units: NumericSuffix::Mm,
8348                }),
8349            },
8350            center: Point2d {
8351                x: Expr::Var(Number {
8352                    value: 13.0,
8353                    units: NumericSuffix::Mm,
8354                }),
8355                y: Expr::Var(Number {
8356                    value: 2.0,
8357                    units: NumericSuffix::Mm,
8358                }),
8359            },
8360            direction: None,
8361            construction: None,
8362        };
8363        let segments = vec![ExistingSegmentCtor {
8364            id: arc,
8365            ctor: SegmentCtor::Arc(arc_ctor),
8366        }];
8367        let (src_delta, scene_delta) = frontend
8368            .edit_segments(&mock_ctx, version, sketch_id, segments)
8369            .await
8370            .unwrap();
8371        insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_2", src_delta.text.as_str());
8372        assert_eq!(scene_delta.new_objects, vec![]);
8373        assert_eq!(scene_delta.new_graph.objects.len(), 6);
8374
8375        ctx.close().await;
8376        mock_ctx.close().await;
8377    }
8378
8379    #[tokio::test(flavor = "multi_thread")]
8380    async fn test_new_sketch_add_circle_edit_circle() {
8381        let program = Program::empty();
8382
8383        let mut frontend = FrontendState::new();
8384        frontend.program = program;
8385
8386        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8387        let mock_ctx = ExecutorContext::new_mock(None).await;
8388        let version = Version(0);
8389
8390        let sketch_args = SketchCtor {
8391            on: Plane::Default(PlaneName::Xy),
8392        };
8393        let (_src_delta, _scene_delta, sketch_id) = frontend
8394            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8395            .await
8396            .unwrap();
8397
8398        // Add a circle segment.
8399        let circle_ctor = CircleCtor {
8400            start: Point2d {
8401                x: Expr::Var(Number {
8402                    value: 5.0,
8403                    units: NumericSuffix::Mm,
8404                }),
8405                y: Expr::Var(Number {
8406                    value: 0.0,
8407                    units: NumericSuffix::Mm,
8408                }),
8409            },
8410            center: Point2d {
8411                x: Expr::Var(Number {
8412                    value: 0.0,
8413                    units: NumericSuffix::Mm,
8414                }),
8415                y: Expr::Var(Number {
8416                    value: 0.0,
8417                    units: NumericSuffix::Mm,
8418                }),
8419            },
8420            construction: None,
8421        };
8422        let segment = SegmentCtor::Circle(circle_ctor);
8423        let (src_delta, scene_delta) = frontend
8424            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8425            .await
8426            .unwrap();
8427        insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_1", src_delta.text.as_str());
8428        // The new objects are start, center, and then the circle segment.
8429        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8430        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8431
8432        let circle = *scene_delta.new_objects.last().unwrap();
8433
8434        // Edit the circle segment.
8435        let circle_ctor = CircleCtor {
8436            start: Point2d {
8437                x: Expr::Var(Number {
8438                    value: 10.0,
8439                    units: NumericSuffix::Mm,
8440                }),
8441                y: Expr::Var(Number {
8442                    value: 0.0,
8443                    units: NumericSuffix::Mm,
8444                }),
8445            },
8446            center: Point2d {
8447                x: Expr::Var(Number {
8448                    value: 3.0,
8449                    units: NumericSuffix::Mm,
8450                }),
8451                y: Expr::Var(Number {
8452                    value: 4.0,
8453                    units: NumericSuffix::Mm,
8454                }),
8455            },
8456            construction: None,
8457        };
8458        let segments = vec![ExistingSegmentCtor {
8459            id: circle,
8460            ctor: SegmentCtor::Circle(circle_ctor),
8461        }];
8462        let (src_delta, scene_delta) = frontend
8463            .edit_segments(&mock_ctx, version, sketch_id, segments)
8464            .await
8465            .unwrap();
8466        insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_2", src_delta.text.as_str());
8467        assert_eq!(scene_delta.new_objects, vec![]);
8468        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8469
8470        ctx.close().await;
8471        mock_ctx.close().await;
8472    }
8473
8474    #[tokio::test(flavor = "multi_thread")]
8475    async fn test_delete_circle() {
8476        let initial_source = "sketch001 = sketch(on = XY) {
8477  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8478}
8479";
8480
8481        let program = Program::parse(initial_source).unwrap().0.unwrap();
8482        let mut frontend = FrontendState::new();
8483
8484        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8485        let mock_ctx = ExecutorContext::new_mock(None).await;
8486        let version = Version(0);
8487
8488        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8489        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8490        let sketch_id = sketch_object.id;
8491        let sketch = expect_sketch(sketch_object);
8492
8493        // The sketch should have 3 segments: start point, center point, and the circle.
8494        assert_eq!(sketch.segments.len(), 3);
8495        let circle_id = sketch.segments[2];
8496
8497        // Delete the circle.
8498        let (src_delta, scene_delta) = frontend
8499            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
8500            .await
8501            .unwrap();
8502        insta::assert_snapshot!("test_delete_circle", src_delta.text.as_str());
8503        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8504        let new_sketch = expect_sketch(new_sketch_object);
8505        assert_eq!(new_sketch.segments.len(), 0);
8506
8507        ctx.close().await;
8508        mock_ctx.close().await;
8509    }
8510
8511    #[tokio::test(flavor = "multi_thread")]
8512    async fn test_edit_circle_via_point() {
8513        let initial_source = "sketch001 = sketch(on = XY) {
8514  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8515}
8516";
8517
8518        let program = Program::parse(initial_source).unwrap().0.unwrap();
8519        let mut frontend = FrontendState::new();
8520
8521        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8522        let mock_ctx = ExecutorContext::new_mock(None).await;
8523        let version = Version(0);
8524
8525        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8526        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8527        let sketch_id = sketch_object.id;
8528        let sketch = expect_sketch(sketch_object);
8529
8530        // Find the circle segment and its start point.
8531        let circle_id = sketch
8532            .segments
8533            .iter()
8534            .copied()
8535            .find(|seg_id| {
8536                matches!(
8537                    &frontend.scene_graph.objects[seg_id.0].kind,
8538                    ObjectKind::Segment {
8539                        segment: Segment::Circle(_)
8540                    }
8541                )
8542            })
8543            .expect("Expected a circle segment in sketch");
8544        let circle_object = &frontend.scene_graph.objects[circle_id.0];
8545        let ObjectKind::Segment {
8546            segment: Segment::Circle(circle),
8547        } = &circle_object.kind
8548        else {
8549            panic!("Expected circle segment, got: {:?}", circle_object.kind);
8550        };
8551        let start_point_id = circle.start;
8552
8553        // Edit the start point via SegmentCtor::Point.
8554        let segments = vec![ExistingSegmentCtor {
8555            id: start_point_id,
8556            ctor: SegmentCtor::Point(PointCtor {
8557                position: Point2d {
8558                    x: Expr::Var(Number {
8559                        value: 7.0,
8560                        units: NumericSuffix::Mm,
8561                    }),
8562                    y: Expr::Var(Number {
8563                        value: 1.0,
8564                        units: NumericSuffix::Mm,
8565                    }),
8566                },
8567            }),
8568        }];
8569        let (src_delta, _scene_delta) = frontend
8570            .edit_segments(&mock_ctx, version, sketch_id, segments)
8571            .await
8572            .unwrap();
8573        insta::assert_snapshot!("test_edit_circle_via_point", src_delta.text.as_str());
8574
8575        ctx.close().await;
8576        mock_ctx.close().await;
8577    }
8578
8579    #[tokio::test(flavor = "multi_thread")]
8580    async fn test_add_line_when_sketch_block_uses_variable() {
8581        let initial_source = "s = sketch(on = XY) {}
8582";
8583
8584        let program = Program::parse(initial_source).unwrap().0.unwrap();
8585
8586        let mut frontend = FrontendState::new();
8587
8588        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8589        let mock_ctx = ExecutorContext::new_mock(None).await;
8590        let version = Version(0);
8591
8592        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8593        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8594        let sketch_id = sketch_object.id;
8595
8596        let line_ctor = LineCtor {
8597            start: Point2d {
8598                x: Expr::Number(Number {
8599                    value: 0.0,
8600                    units: NumericSuffix::Mm,
8601                }),
8602                y: Expr::Number(Number {
8603                    value: 0.0,
8604                    units: NumericSuffix::Mm,
8605                }),
8606            },
8607            end: Point2d {
8608                x: Expr::Number(Number {
8609                    value: 10.0,
8610                    units: NumericSuffix::Mm,
8611                }),
8612                y: Expr::Number(Number {
8613                    value: 10.0,
8614                    units: NumericSuffix::Mm,
8615                }),
8616            },
8617            construction: None,
8618        };
8619        let segment = SegmentCtor::Line(line_ctor);
8620        let (src_delta, scene_delta) = frontend
8621            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8622            .await
8623            .unwrap();
8624        insta::assert_snapshot!("test_add_line_when_sketch_block_uses_variable", src_delta.text.as_str());
8625        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8626        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8627
8628        ctx.close().await;
8629        mock_ctx.close().await;
8630    }
8631
8632    #[tokio::test(flavor = "multi_thread")]
8633    async fn test_new_sketch_add_line_delete_sketch() {
8634        let program = Program::empty();
8635
8636        let mut frontend = FrontendState::new();
8637        frontend.program = program;
8638
8639        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8640        let mock_ctx = ExecutorContext::new_mock(None).await;
8641        let version = Version(0);
8642
8643        let sketch_args = SketchCtor {
8644            on: Plane::Default(PlaneName::Xy),
8645        };
8646        let (_src_delta, scene_delta, sketch_id) = frontend
8647            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8648            .await
8649            .unwrap();
8650        assert_eq!(sketch_id, ObjectId(1));
8651        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8652        let sketch_object = &scene_delta.new_graph.objects[1];
8653        assert_eq!(sketch_object.id, ObjectId(1));
8654        assert_eq!(
8655            sketch_object.kind,
8656            ObjectKind::Sketch(Sketch {
8657                args: SketchCtor {
8658                    on: Plane::Default(PlaneName::Xy)
8659                },
8660                plane: ObjectId(0),
8661                segments: vec![],
8662                constraints: vec![],
8663            })
8664        );
8665        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8666
8667        let line_ctor = LineCtor {
8668            start: Point2d {
8669                x: Expr::Number(Number {
8670                    value: 0.0,
8671                    units: NumericSuffix::Mm,
8672                }),
8673                y: Expr::Number(Number {
8674                    value: 0.0,
8675                    units: NumericSuffix::Mm,
8676                }),
8677            },
8678            end: Point2d {
8679                x: Expr::Number(Number {
8680                    value: 10.0,
8681                    units: NumericSuffix::Mm,
8682                }),
8683                y: Expr::Number(Number {
8684                    value: 10.0,
8685                    units: NumericSuffix::Mm,
8686                }),
8687            },
8688            construction: None,
8689        };
8690        let segment = SegmentCtor::Line(line_ctor);
8691        let (src_delta, scene_delta) = frontend
8692            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8693            .await
8694            .unwrap();
8695        insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_1", src_delta.text.as_str());
8696        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8697
8698        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8699        insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_2", src_delta.text.as_str());
8700        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8701
8702        ctx.close().await;
8703        mock_ctx.close().await;
8704    }
8705
8706    #[tokio::test(flavor = "multi_thread")]
8707    async fn test_delete_sketch_when_sketch_block_uses_variable() {
8708        let initial_source = "s = sketch(on = XY) {}
8709";
8710
8711        let program = Program::parse(initial_source).unwrap().0.unwrap();
8712
8713        let mut frontend = FrontendState::new();
8714
8715        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8716        let version = Version(0);
8717
8718        frontend.hack_set_program(&ctx, program).await.unwrap();
8719        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8720        let sketch_id = sketch_object.id;
8721
8722        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8723        insta::assert_snapshot!(
8724            "test_delete_sketch_when_sketch_block_uses_variable",
8725            src_delta.text.as_str()
8726        );
8727        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8728
8729        ctx.close().await;
8730    }
8731
8732    #[tokio::test(flavor = "multi_thread")]
8733    async fn test_delete_sketch_after_comment() {
8734        let initial_source = "sketch001 = sketch(on = XZ) {
8735}
8736";
8737
8738        let program = Program::parse(initial_source).unwrap().0.unwrap();
8739        let mut frontend = FrontendState::new();
8740
8741        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8742        let version = Version(0);
8743
8744        frontend.hack_set_program(&ctx, program).await.unwrap();
8745        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8746        let sketch_id = sketch_object.id;
8747        let original_source = sketch_object.source.clone();
8748
8749        let commented_source = "// test 1
8750sketch001 = sketch(on = XZ) {
8751}
8752";
8753        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8754        frontend.engine_execute(&ctx, commented_program).await.unwrap();
8755
8756        let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
8757        assert_eq!(cached_sketch_object.source, original_source);
8758
8759        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8760        assert!(
8761            !src_delta.text.contains("sketch001"),
8762            "sketch was not deleted: {}",
8763            src_delta.text
8764        );
8765        // The leading line comment must survive deletion.
8766        insta::assert_snapshot!("test_delete_sketch_after_comment", src_delta.text.as_str());
8767        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8768
8769        ctx.close().await;
8770    }
8771
8772    #[tokio::test(flavor = "multi_thread")]
8773    async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
8774        let initial_source = "sketch001 = sketch(on = XZ) {
8775}
8776foo = 1
8777";
8778
8779        let program = Program::parse(initial_source).unwrap().0.unwrap();
8780        let mut frontend = FrontendState::new();
8781
8782        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8783        let version = Version(0);
8784
8785        frontend.hack_set_program(&ctx, program).await.unwrap();
8786        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8787        let sketch_id = sketch_object.id;
8788
8789        let commented_source = "// keep me
8790sketch001 = sketch(on = XZ) {
8791}
8792foo = 1
8793";
8794        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8795        frontend.engine_execute(&ctx, commented_program).await.unwrap();
8796
8797        let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8798        // The leading comment should remain, now attached to the following body item.
8799        insta::assert_snapshot!(
8800            "test_delete_sketch_preserves_pre_comment_when_followed_by_code",
8801            src_delta.text.as_str()
8802        );
8803
8804        ctx.close().await;
8805    }
8806
8807    #[tokio::test(flavor = "multi_thread")]
8808    async fn test_delete_segment_preserves_pre_comment() {
8809        let initial_source = "\
8810sketch(on = XY) {
8811  point(at = [var 1, var 2])
8812  // describe the middle point
8813  point(at = [var 3, var 4])
8814  point(at = [var 5, var 6])
8815}
8816";
8817
8818        let program = Program::parse(initial_source).unwrap().0.unwrap();
8819        let mut frontend = FrontendState::new();
8820
8821        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8822        let mock_ctx = ExecutorContext::new_mock(None).await;
8823        let version = Version(0);
8824
8825        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8826        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8827        let sketch_id = sketch_object.id;
8828        let sketch = expect_sketch(sketch_object);
8829
8830        let middle_point_id = *sketch.segments.get(1).unwrap();
8831
8832        let (src_delta, _scene_delta) = frontend
8833            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8834            .await
8835            .unwrap();
8836        // The line comment on the line above the deleted point must be preserved.
8837        // It is reattached to the next surviving body item.
8838        insta::assert_snapshot!("test_delete_segment_preserves_pre_comment", src_delta.text.as_str());
8839
8840        ctx.close().await;
8841        mock_ctx.close().await;
8842    }
8843
8844    #[tokio::test(flavor = "multi_thread")]
8845    async fn test_delete_last_segment_preserves_pre_comment() {
8846        let initial_source = "\
8847sketch(on = XY) {
8848  point(at = [var 1, var 2])
8849  // describe the trailing point
8850  point(at = [var 3, var 4])
8851}
8852";
8853
8854        let program = Program::parse(initial_source).unwrap().0.unwrap();
8855        let mut frontend = FrontendState::new();
8856
8857        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8858        let mock_ctx = ExecutorContext::new_mock(None).await;
8859        let version = Version(0);
8860
8861        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8862        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8863        let sketch_id = sketch_object.id;
8864        let sketch = expect_sketch(sketch_object);
8865
8866        let last_point_id = *sketch.segments.last().unwrap();
8867
8868        let (src_delta, _scene_delta) = frontend
8869            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
8870            .await
8871            .unwrap();
8872        // No following item to attach to; the comment is kept inside the sketch
8873        // block as trailing non-code metadata so the user does not lose it.
8874        insta::assert_snapshot!(
8875            "test_delete_last_segment_preserves_pre_comment",
8876            src_delta.text.as_str()
8877        );
8878
8879        ctx.close().await;
8880        mock_ctx.close().await;
8881    }
8882
8883    #[tokio::test(flavor = "multi_thread")]
8884    async fn test_delete_segment_drops_inline_trailing_comment() {
8885        let initial_source = "\
8886sketch(on = XY) {
8887  point(at = [var 1, var 2])
8888  point(at = [var 3, var 4]) // same-line note that gets dropped
8889  point(at = [var 5, var 6])
8890}
8891";
8892
8893        let program = Program::parse(initial_source).unwrap().0.unwrap();
8894        let mut frontend = FrontendState::new();
8895
8896        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8897        let mock_ctx = ExecutorContext::new_mock(None).await;
8898        let version = Version(0);
8899
8900        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8901        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8902        let sketch_id = sketch_object.id;
8903        let sketch = expect_sketch(sketch_object);
8904
8905        let middle_point_id = *sketch.segments.get(1).unwrap();
8906
8907        let (src_delta, _scene_delta) = frontend
8908            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8909            .await
8910            .unwrap();
8911        // The same-line trailing comment is removed along with the deleted code.
8912        assert!(
8913            !src_delta.text.contains("same-line note"),
8914            "inline comment should have been removed: {}",
8915            src_delta.text
8916        );
8917
8918        ctx.close().await;
8919        mock_ctx.close().await;
8920    }
8921
8922    #[tokio::test(flavor = "multi_thread")]
8923    async fn test_delete_segments_preserves_block_comments_across_positions() {
8924        // One test exercising several `delete_body_item_preserving_pre_comments`
8925        // branches at once with `/* ... */` block comments:
8926        //   - first point: leading block comment must migrate to the next item.
8927        //   - first point: same-line trailing block comment must be dropped.
8928        //   - middle point: leading block comment must stay attached after migration.
8929        //   - last point: leading block comment, with no surviving next item,
8930        //     must be converted into a trailing NonCodeNode.
8931        let initial_source = "\
8932sketch(on = XY) {
8933  /* above first - moves to middle */
8934  point(at = [var 1, var 2]) /* same-line on first - dropped */
8935  /* above middle - stays */
8936  point(at = [var 3, var 4])
8937  /* above last - moves to trailing meta */
8938  point(at = [var 5, var 6])
8939}
8940";
8941
8942        let program = Program::parse(initial_source).unwrap().0.unwrap();
8943        let mut frontend = FrontendState::new();
8944
8945        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8946        let mock_ctx = ExecutorContext::new_mock(None).await;
8947        let version = Version(0);
8948
8949        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8950        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8951        let sketch_id = sketch_object.id;
8952        let sketch = expect_sketch(sketch_object);
8953
8954        let first_point_id = *sketch.segments.first().unwrap();
8955        let last_point_id = *sketch.segments.last().unwrap();
8956
8957        let (src_delta, _scene_delta) = frontend
8958            .delete_objects(
8959                &mock_ctx,
8960                version,
8961                sketch_id,
8962                Vec::new(),
8963                vec![first_point_id, last_point_id],
8964            )
8965            .await
8966            .unwrap();
8967        insta::assert_snapshot!(
8968            "test_delete_segments_preserves_block_comments_across_positions",
8969            src_delta.text.as_str()
8970        );
8971
8972        ctx.close().await;
8973        mock_ctx.close().await;
8974    }
8975
8976    #[tokio::test(flavor = "multi_thread")]
8977    async fn test_edit_line_when_editing_its_start_point() {
8978        let initial_source = "\
8979sketch(on = XY) {
8980  line(start = [var 1, var 2], end = [var 3, var 4])
8981}
8982";
8983
8984        let program = Program::parse(initial_source).unwrap().0.unwrap();
8985
8986        let mut frontend = FrontendState::new();
8987
8988        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8989        let mock_ctx = ExecutorContext::new_mock(None).await;
8990        let version = Version(0);
8991
8992        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8993        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8994        let sketch_id = sketch_object.id;
8995        let sketch = expect_sketch(sketch_object);
8996
8997        let point_id = *sketch.segments.first().unwrap();
8998
8999        let point_ctor = PointCtor {
9000            position: Point2d {
9001                x: Expr::Var(Number {
9002                    value: 5.0,
9003                    units: NumericSuffix::Inch,
9004                }),
9005                y: Expr::Var(Number {
9006                    value: 6.0,
9007                    units: NumericSuffix::Inch,
9008                }),
9009            },
9010        };
9011        let segments = vec![ExistingSegmentCtor {
9012            id: point_id,
9013            ctor: SegmentCtor::Point(point_ctor),
9014        }];
9015        let (src_delta, scene_delta) = frontend
9016            .edit_segments(&mock_ctx, version, sketch_id, segments)
9017            .await
9018            .unwrap();
9019        insta::assert_snapshot!("test_edit_line_when_editing_its_start_point", src_delta.text.as_str());
9020        assert_eq!(scene_delta.new_objects, vec![]);
9021        assert_eq!(scene_delta.new_graph.objects.len(), 5);
9022
9023        ctx.close().await;
9024        mock_ctx.close().await;
9025    }
9026
9027    #[tokio::test(flavor = "multi_thread")]
9028    async fn test_edit_line_when_editing_its_end_point() {
9029        let initial_source = "\
9030sketch(on = XY) {
9031  line(start = [var 1, var 2], end = [var 3, var 4])
9032}
9033";
9034
9035        let program = Program::parse(initial_source).unwrap().0.unwrap();
9036
9037        let mut frontend = FrontendState::new();
9038
9039        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9040        let mock_ctx = ExecutorContext::new_mock(None).await;
9041        let version = Version(0);
9042
9043        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9044        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9045        let sketch_id = sketch_object.id;
9046        let sketch = expect_sketch(sketch_object);
9047        let point_id = *sketch.segments.get(1).unwrap();
9048
9049        let point_ctor = PointCtor {
9050            position: Point2d {
9051                x: Expr::Var(Number {
9052                    value: 5.0,
9053                    units: NumericSuffix::Inch,
9054                }),
9055                y: Expr::Var(Number {
9056                    value: 6.0,
9057                    units: NumericSuffix::Inch,
9058                }),
9059            },
9060        };
9061        let segments = vec![ExistingSegmentCtor {
9062            id: point_id,
9063            ctor: SegmentCtor::Point(point_ctor),
9064        }];
9065        let (src_delta, scene_delta) = frontend
9066            .edit_segments(&mock_ctx, version, sketch_id, segments)
9067            .await
9068            .unwrap();
9069        insta::assert_snapshot!("test_edit_line_when_editing_its_end_point", src_delta.text.as_str());
9070        assert_eq!(scene_delta.new_objects, vec![]);
9071        assert_eq!(
9072            scene_delta.new_graph.objects.len(),
9073            5,
9074            "{:#?}",
9075            scene_delta.new_graph.objects
9076        );
9077
9078        ctx.close().await;
9079        mock_ctx.close().await;
9080    }
9081
9082    #[tokio::test(flavor = "multi_thread")]
9083    async fn test_edit_line_with_coincident_feedback() {
9084        let initial_source = "\
9085sketch(on = XY) {
9086  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9087  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9088  fixed([line1.start, [0, 0]])
9089  coincident([line1.end, line2.start])
9090  equalLength([line1, line2])
9091}
9092";
9093
9094        let program = Program::parse(initial_source).unwrap().0.unwrap();
9095
9096        let mut frontend = FrontendState::new();
9097
9098        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9099        let mock_ctx = ExecutorContext::new_mock(None).await;
9100        let version = Version(0);
9101
9102        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9103        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9104        let sketch_id = sketch_object.id;
9105        let sketch = expect_sketch(sketch_object);
9106        let line2_end_id = *sketch.segments.get(4).unwrap();
9107
9108        let segments = vec![ExistingSegmentCtor {
9109            id: line2_end_id,
9110            ctor: SegmentCtor::Point(PointCtor {
9111                position: Point2d {
9112                    x: Expr::Var(Number {
9113                        value: 9.0,
9114                        units: NumericSuffix::None,
9115                    }),
9116                    y: Expr::Var(Number {
9117                        value: 10.0,
9118                        units: NumericSuffix::None,
9119                    }),
9120                },
9121            }),
9122        }];
9123        let (src_delta, scene_delta) = frontend
9124            .edit_segments(&mock_ctx, version, sketch_id, segments)
9125            .await
9126            .unwrap();
9127        insta::assert_snapshot!("test_edit_line_with_coincident_feedback", src_delta.text.as_str());
9128        assert_eq!(
9129            scene_delta.new_graph.objects.len(),
9130            11,
9131            "{:#?}",
9132            scene_delta.new_graph.objects
9133        );
9134
9135        ctx.close().await;
9136        mock_ctx.close().await;
9137    }
9138
9139    #[tokio::test(flavor = "multi_thread")]
9140    async fn test_edit_segments_persists_solver_feedback_for_next_mock_execute() {
9141        let initial_source = "\
9142sketch(on = XY) {
9143  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9144  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9145  fixed([line1.start, [0, 0]])
9146  coincident([line1.end, line2.start])
9147  equalLength([line1, line2])
9148}
9149";
9150
9151        let program = Program::parse(initial_source).unwrap().0.unwrap();
9152        let mut frontend = FrontendState::new();
9153        let mock_ctx = ExecutorContext::new_mock(None).await;
9154        let version = Version(0);
9155
9156        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9157        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9158        let sketch_id = sketch_object.id;
9159        let sketch = expect_sketch(sketch_object);
9160        let line2_end_id = *sketch.segments.get(4).unwrap();
9161
9162        let segments = vec![ExistingSegmentCtor {
9163            id: line2_end_id,
9164            ctor: SegmentCtor::Point(PointCtor {
9165                position: Point2d {
9166                    x: Expr::Var(Number {
9167                        value: 9.0,
9168                        units: NumericSuffix::None,
9169                    }),
9170                    y: Expr::Var(Number {
9171                        value: 10.0,
9172                        units: NumericSuffix::None,
9173                    }),
9174                },
9175            }),
9176        }];
9177        let (edited_source, _) = frontend
9178            .edit_segments(&mock_ctx, version, sketch_id, segments)
9179            .await
9180            .unwrap();
9181
9182        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9183        assert_eq!(mock_source.text, edited_source.text);
9184
9185        mock_ctx.close().await;
9186    }
9187
9188    /// Preview segment edits should return solved geometry without persisting
9189    /// solver feedback to KCL.
9190    #[tokio::test(flavor = "multi_thread")]
9191    async fn test_preview_edit_segments_does_not_persist_solver_feedback() {
9192        let initial_source = "\
9193sketch(on = XY) {
9194  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9195  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9196  fixed([line1.start, [0, 0]])
9197  coincident([line1.end, line2.start])
9198  equalLength([line1, line2])
9199}
9200";
9201
9202        let program = Program::parse(initial_source).unwrap().0.unwrap();
9203        let mut frontend = FrontendState::new();
9204        let mock_ctx = ExecutorContext::new_mock(None).await;
9205        let version = Version(0);
9206
9207        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9208        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9209        let sketch_id = sketch_object.id;
9210        let sketch = expect_sketch(sketch_object);
9211        let line2_end_id = *sketch.segments.get(4).unwrap();
9212
9213        let segments = vec![ExistingSegmentCtor {
9214            id: line2_end_id,
9215            ctor: SegmentCtor::Point(PointCtor {
9216                position: Point2d {
9217                    x: Expr::Var(Number {
9218                        value: 9.0,
9219                        units: NumericSuffix::None,
9220                    }),
9221                    y: Expr::Var(Number {
9222                        value: 10.0,
9223                        units: NumericSuffix::None,
9224                    }),
9225                },
9226            }),
9227        }];
9228        let (preview_source, preview_delta) = frontend
9229            .edit_segments_with_options(
9230                &mock_ctx,
9231                version,
9232                sketch_id,
9233                segments,
9234                EditSegmentsOptions {
9235                    anchor_segment_ids: Some(vec![line2_end_id]),
9236                    drag_anchors: Vec::new(),
9237                    constraint_label_edits: Vec::new(),
9238                    commit_solved_initial_guesses: false,
9239                },
9240            )
9241            .await
9242            .unwrap();
9243
9244        assert!(
9245            !preview_delta.exec_outcome.var_solutions.is_empty(),
9246            "preview solve should still solve and return geometry feedback"
9247        );
9248        assert!(
9249            preview_source
9250                .text
9251                .contains("line1 = line(start = [var 1, var 2], end = [var 1, var 2])")
9252        );
9253        assert!(
9254            preview_source
9255                .text
9256                .contains("line2 = line(start = [var 5, var 6], end = [var 9, var 10])")
9257        );
9258
9259        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9260        assert_eq!(mock_source.text, preview_source.text);
9261
9262        mock_ctx.close().await;
9263    }
9264
9265    #[tokio::test(flavor = "multi_thread")]
9266    async fn test_add_constraint_persists_solver_feedback_for_next_mock_execute() {
9267        let initial_source = "\
9268sketch(on = XY) {
9269  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
9270}
9271";
9272
9273        let program = Program::parse(initial_source).unwrap().0.unwrap();
9274        let mut frontend = FrontendState::new();
9275        let mock_ctx = ExecutorContext::new_mock(None).await;
9276        let version = Version(0);
9277
9278        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9279        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9280        let sketch_id = sketch_object.id;
9281        let sketch = expect_sketch(sketch_object);
9282        let line_end_id = *sketch.segments.get(1).unwrap();
9283
9284        let constraint = Constraint::Fixed(Fixed {
9285            points: vec![FixedPoint {
9286                point: line_end_id,
9287                position: Point2d {
9288                    x: Number {
9289                        value: 20.0,
9290                        units: NumericSuffix::Mm,
9291                    },
9292                    y: Number {
9293                        value: 0.0,
9294                        units: NumericSuffix::Mm,
9295                    },
9296                },
9297            }],
9298        });
9299        let (constraint_source, _) = frontend
9300            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9301            .await
9302            .unwrap();
9303
9304        assert!(
9305            constraint_source
9306                .text
9307                .contains("line1 = line(start = [var 0, var 0], end = [var 20, var 0])"),
9308            "{}",
9309            constraint_source.text
9310        );
9311        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9312        assert_eq!(mock_source.text, constraint_source.text);
9313
9314        mock_ctx.close().await;
9315    }
9316
9317    #[test]
9318    fn test_no_solver_feedback_preserves_original_source() {
9319        let initial_source = "\
9320@settings(defaultLengthUnit = in, kclVersion = 2.0)
9321cylinder = startSketchOn(XY)
9322    |> circle(center= [0, 0], radius= 22)
9323    |> extrude(length = 14)
9324";
9325        let mut frontend = FrontendState::new();
9326        frontend.program = Program::parse(initial_source).unwrap().0.unwrap();
9327        let outcome = ExecOutcome {
9328            variables: Default::default(),
9329            operations: Default::default(),
9330            artifact_graph: Default::default(),
9331            scene_objects: Default::default(),
9332            source_range_to_object: Default::default(),
9333            var_solutions: Default::default(),
9334            refactor_metadata: Default::default(),
9335            issues: Default::default(),
9336            filenames: Default::default(),
9337            default_planes: Default::default(),
9338        };
9339
9340        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9341
9342        assert_eq!(source_delta.text, initial_source);
9343    }
9344
9345    /// Explicit drag anchors should limit which edited points become temporary
9346    /// fixed constraints.
9347    #[tokio::test(flavor = "multi_thread")]
9348    async fn test_edit_segments_with_anchor_ids_limits_drag_fixed_constraints() {
9349        let initial_source = "\
9350sketch(on = XY) {
9351  point1 = point(at = [var 0mm, var 0mm])
9352  point2 = point(at = [var 0mm, var 0mm])
9353  coincident([point1, point2])
9354}
9355";
9356
9357        let program = Program::parse(initial_source).unwrap().0.unwrap();
9358        let mut frontend = FrontendState::new();
9359        let mock_ctx = ExecutorContext::new_mock(None).await;
9360        let version = Version(0);
9361
9362        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9363        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9364        let sketch_id = sketch_object.id;
9365        let sketch = expect_sketch(sketch_object);
9366        let point1_id = sketch.segments[0];
9367        let point2_id = sketch.segments[1];
9368
9369        let segments = vec![
9370            ExistingSegmentCtor {
9371                id: point1_id,
9372                ctor: SegmentCtor::Point(PointCtor {
9373                    position: point_expr_mm(10.0, 0.0),
9374                }),
9375            },
9376            ExistingSegmentCtor {
9377                id: point2_id,
9378                ctor: SegmentCtor::Point(PointCtor {
9379                    position: point_expr_mm(100.0, 0.0),
9380                }),
9381            },
9382        ];
9383        let (_, scene_delta) = frontend
9384            .edit_segments_with_options(
9385                &mock_ctx,
9386                version,
9387                sketch_id,
9388                segments,
9389                EditSegmentsOptions {
9390                    anchor_segment_ids: Some(vec![point1_id]),
9391                    drag_anchors: Vec::new(),
9392                    constraint_label_edits: Vec::new(),
9393                    commit_solved_initial_guesses: true,
9394                },
9395            )
9396            .await
9397            .unwrap();
9398
9399        assert_point_position_close(
9400            point_position(&scene_delta.new_graph, point1_id),
9401            point_number_mm(10.0, 0.0),
9402        );
9403        assert_point_position_close(
9404            point_position(&scene_delta.new_graph, point2_id),
9405            point_number_mm(10.0, 0.0),
9406        );
9407
9408        mock_ctx.close().await;
9409    }
9410
9411    /// Walks a program collecting `(literal_source_range, sketch_var_node_path)`
9412    /// for every SketchVar whose initial NumericLiteral has the given value.
9413    fn collect_sketch_var_literals_with_value(program: &Program, value: f64) -> Vec<(SourceRange, ast::NodePath)> {
9414        use std::cell::RefCell;
9415        struct Collector {
9416            target: f64,
9417            out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9418        }
9419        impl<'a> crate::walk::Visitor<'a> for &Collector {
9420            type Error = crate::front::Error;
9421            fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9422                if let crate::walk::Node::SketchVar(sketch_var) = node
9423                    && let (Some(initial), Some(node_path)) = (&sketch_var.initial, &sketch_var.node_path)
9424                    && (initial.value - self.target).abs() < 1e-9
9425                {
9426                    self.out
9427                        .borrow_mut()
9428                        .push((SourceRange::from(initial.as_ref()), node_path.clone()));
9429                }
9430                for child in node.children().iter() {
9431                    if !child.visit(*self)? {
9432                        return Ok(false);
9433                    }
9434                }
9435                Ok(true)
9436            }
9437        }
9438        let collector = Collector {
9439            target: value,
9440            out: Default::default(),
9441        };
9442        let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9443        collector.out.into_inner()
9444    }
9445
9446    /// Walk a program collecting `(sketch_var_source_range, sketch_var_node_path)`
9447    /// for every SketchVar (including bare `var`).
9448    fn collect_all_sketch_vars(program: &Program) -> Vec<(SourceRange, ast::NodePath)> {
9449        use std::cell::RefCell;
9450        struct Collector {
9451            out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9452        }
9453        impl<'a> crate::walk::Visitor<'a> for &Collector {
9454            type Error = crate::front::Error;
9455            fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9456                if let crate::walk::Node::SketchVar(sketch_var) = node
9457                    && let Some(node_path) = &sketch_var.node_path
9458                {
9459                    self.out
9460                        .borrow_mut()
9461                        .push((SourceRange::from(sketch_var), node_path.clone()));
9462                }
9463                for child in node.children().iter() {
9464                    if !child.visit(*self)? {
9465                        return Ok(false);
9466                    }
9467                }
9468                Ok(true)
9469            }
9470        }
9471        let collector = Collector {
9472            out: Default::default(),
9473        };
9474        let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9475        collector.out.into_inner()
9476    }
9477
9478    fn empty_exec_outcome_with_var_solutions(
9479        var_solutions: Vec<(SourceRange, Option<ast::NodePath>, Number)>,
9480    ) -> ExecOutcome {
9481        ExecOutcome {
9482            variables: Default::default(),
9483            operations: Default::default(),
9484            artifact_graph: Default::default(),
9485            scene_objects: Default::default(),
9486            source_range_to_object: Default::default(),
9487            var_solutions,
9488            refactor_metadata: Default::default(),
9489            issues: Default::default(),
9490            filenames: Default::default(),
9491            default_planes: Default::default(),
9492        }
9493    }
9494
9495    /// Happy path: commit a var solution to a `var N` inside a sketch block
9496    /// using a correct NodePath. Confirms the node-path code path produces the
9497    /// expected source mutation.
9498    #[test]
9499    fn test_commit_var_solution_by_node_path_updates_sketch_var() {
9500        let initial_source = "\
9501sketch(on = XY) {
9502  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9503}
9504";
9505        let program = Program::parse(initial_source).unwrap().0.unwrap();
9506        let matches = collect_sketch_var_literals_with_value(&program, 10.0);
9507        assert_eq!(matches.len(), 1, "expected exactly one `var 10mm`");
9508        let (literal_range, node_path) = matches.into_iter().next().unwrap();
9509
9510        let mut frontend = FrontendState::new();
9511        frontend.program = program;
9512
9513        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9514            literal_range,
9515            Some(node_path),
9516            Number {
9517                value: 25.0,
9518                units: NumericSuffix::Mm,
9519            },
9520        )]);
9521
9522        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9523
9524        insta::assert_snapshot!(
9525            "test_commit_var_solution_by_node_path_updates_sketch_var",
9526            source_delta.text
9527        );
9528    }
9529
9530    /// Whitespace inserted earlier in the source shifts the original SketchVar
9531    /// SourceRange. With NodePath propagation the commit should still target
9532    /// the right `var`. We simulate this by collecting node_paths against a
9533    /// "compact" source, then loading the frontend with a "padded" source
9534    /// (whose byte offsets differ), and feeding the original (now stale)
9535    /// source range plus the correct node_path back into the commit.
9536    #[test]
9537    fn test_commit_var_solution_survives_whitespace_shift_earlier_in_file() {
9538        let compact_source = "\
9539sketch(on = XY) {
9540  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9541}
9542";
9543        let padded_source = "\
9544// added comment\n// added comment\n\nsketch(on = XY) {
9545  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9546}
9547";
9548        let compact_program = Program::parse(compact_source).unwrap().0.unwrap();
9549        let padded_program = Program::parse(padded_source).unwrap().0.unwrap();
9550
9551        let compact_match = collect_sketch_var_literals_with_value(&compact_program, 10.0)
9552            .into_iter()
9553            .next()
9554            .expect("expected `var 10mm` in compact source");
9555        let padded_match = collect_sketch_var_literals_with_value(&padded_program, 10.0)
9556            .into_iter()
9557            .next()
9558            .expect("expected `var 10mm` in padded source");
9559
9560        assert_ne!(
9561            compact_match.0, padded_match.0,
9562            "byte offsets must differ for this test to be meaningful"
9563        );
9564        assert_eq!(
9565            compact_match.1, padded_match.1,
9566            "node paths must agree across whitespace; that's the whole point of NodePath",
9567        );
9568
9569        let mut frontend = FrontendState::new();
9570        frontend.program = padded_program;
9571
9572        // Stale source range from the compact source + correct node_path.
9573        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9574            compact_match.0,
9575            Some(compact_match.1),
9576            Number {
9577                value: 30.0,
9578                units: NumericSuffix::Mm,
9579            },
9580        )]);
9581
9582        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9583
9584        insta::assert_snapshot!(
9585            "test_commit_var_solution_survives_whitespace_shift_earlier_in_file",
9586            source_delta.text
9587        );
9588    }
9589
9590    /// When multiple `var` declarations exist and the stale source range
9591    /// happens to land on a *different* var, the node_path must take
9592    /// precedence and the right var gets updated.
9593    #[test]
9594    fn test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var() {
9595        let initial_source = "\
9596sketch(on = XY) {
9597  line1 = line(start = [var 10mm, var 0mm], end = [var 20mm, var 0mm])
9598}
9599";
9600        let program = Program::parse(initial_source).unwrap().0.unwrap();
9601
9602        let var_10 = collect_sketch_var_literals_with_value(&program, 10.0)
9603            .into_iter()
9604            .next()
9605            .expect("expected `var 10mm`");
9606        let var_20 = collect_sketch_var_literals_with_value(&program, 20.0)
9607            .into_iter()
9608            .next()
9609            .expect("expected `var 20mm`");
9610
9611        let mut frontend = FrontendState::new();
9612        frontend.program = program;
9613
9614        // Use var 20mm's source range, but var 10mm's node_path. node_path wins.
9615        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9616            var_20.0,
9617            Some(var_10.1),
9618            Number {
9619                value: 33.0,
9620                units: NumericSuffix::Mm,
9621            },
9622        )]);
9623
9624        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9625
9626        insta::assert_snapshot!(
9627            "test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var",
9628            source_delta.text
9629        );
9630    }
9631
9632    /// Bare `var` (no initial literal) is only locatable via node_path. With
9633    /// the EditVarInitialValue handler now operating on the SketchVar node, a
9634    /// solver solution should fill the initial value in. The
9635    /// `@settings(experimentalFeatures = allow)` is required because bare `var`
9636    /// is gated as an experimental feature; without it the re-parse of the
9637    /// recast source rejects bare `var` declarations.
9638    #[test]
9639    fn test_commit_var_solution_writes_back_into_bare_var() {
9640        let initial_source = "\
9641@settings(experimentalFeatures = allow, kclVersion = 2.0)
9642sketch(on = XY) {
9643  line1 = line(start = [var, var 0mm], end = [var 10mm, var 0])
9644}
9645";
9646        let program = Program::parse(initial_source).unwrap().0.unwrap();
9647
9648        // Pick the first bare `var`; collect_all_sketch_vars returns every
9649        // SketchVar, including bare ones.
9650        let bare = collect_all_sketch_vars(&program)
9651            .into_iter()
9652            .find(|(range, _)| {
9653                // The bare `var` is exactly the 3 characters "var".
9654                range.end() - range.start() == 3
9655            })
9656            .expect("expected at least one bare `var`");
9657
9658        let mut frontend = FrontendState::new();
9659        frontend.program = program;
9660
9661        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9662            bare.0,
9663            Some(bare.1),
9664            Number {
9665                value: 7.0,
9666                units: NumericSuffix::Mm,
9667            },
9668        )]);
9669
9670        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9671
9672        // Default length unit (mm; no `@settings(defaultLengthUnit = …)`) is
9673        // written as an explicit suffix so the bare var commits with units.
9674        // The recast adds a blank line after the `@settings` annotation.
9675        insta::assert_snapshot!("test_commit_var_solution_writes_back_into_bare_var", source_delta.text);
9676    }
9677
9678    #[tokio::test(flavor = "multi_thread")]
9679    async fn test_delete_point_without_var() {
9680        let initial_source = "\
9681sketch(on = XY) {
9682  point(at = [var 1, var 2])
9683  point(at = [var 3, var 4])
9684  point(at = [var 5, var 6])
9685}
9686";
9687
9688        let program = Program::parse(initial_source).unwrap().0.unwrap();
9689
9690        let mut frontend = FrontendState::new();
9691
9692        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9693        let mock_ctx = ExecutorContext::new_mock(None).await;
9694        let version = Version(0);
9695
9696        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9697        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9698        let sketch_id = sketch_object.id;
9699        let sketch = expect_sketch(sketch_object);
9700
9701        let point_id = *sketch.segments.get(1).unwrap();
9702
9703        let (src_delta, scene_delta) = frontend
9704            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9705            .await
9706            .unwrap();
9707        insta::assert_snapshot!("test_delete_point_without_var", src_delta.text.as_str());
9708        assert_eq!(scene_delta.new_objects, vec![]);
9709        assert_eq!(scene_delta.new_graph.objects.len(), 4);
9710
9711        ctx.close().await;
9712        mock_ctx.close().await;
9713    }
9714
9715    #[tokio::test(flavor = "multi_thread")]
9716    async fn test_delete_point_with_var() {
9717        let initial_source = "\
9718sketch(on = XY) {
9719  point(at = [var 1, var 2])
9720  point1 = point(at = [var 3, var 4])
9721  point(at = [var 5, var 6])
9722}
9723";
9724
9725        let program = Program::parse(initial_source).unwrap().0.unwrap();
9726
9727        let mut frontend = FrontendState::new();
9728
9729        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9730        let mock_ctx = ExecutorContext::new_mock(None).await;
9731        let version = Version(0);
9732
9733        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9734        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9735        let sketch_id = sketch_object.id;
9736        let sketch = expect_sketch(sketch_object);
9737
9738        let point_id = *sketch.segments.get(1).unwrap();
9739
9740        let (src_delta, scene_delta) = frontend
9741            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9742            .await
9743            .unwrap();
9744        insta::assert_snapshot!("test_delete_point_with_var", src_delta.text.as_str());
9745        assert_eq!(scene_delta.new_objects, vec![]);
9746        assert_eq!(scene_delta.new_graph.objects.len(), 4);
9747
9748        ctx.close().await;
9749        mock_ctx.close().await;
9750    }
9751
9752    #[tokio::test(flavor = "multi_thread")]
9753    async fn test_delete_multiple_points() {
9754        let initial_source = "\
9755sketch(on = XY) {
9756  point(at = [var 1, var 2])
9757  point1 = point(at = [var 3, var 4])
9758  point(at = [var 5, var 6])
9759}
9760";
9761
9762        let program = Program::parse(initial_source).unwrap().0.unwrap();
9763
9764        let mut frontend = FrontendState::new();
9765
9766        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9767        let mock_ctx = ExecutorContext::new_mock(None).await;
9768        let version = Version(0);
9769
9770        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9771        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9772        let sketch_id = sketch_object.id;
9773
9774        let sketch = expect_sketch(sketch_object);
9775
9776        let point1_id = *sketch.segments.first().unwrap();
9777        let point2_id = *sketch.segments.get(1).unwrap();
9778
9779        let (src_delta, scene_delta) = frontend
9780            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
9781            .await
9782            .unwrap();
9783        insta::assert_snapshot!("test_delete_multiple_points", src_delta.text.as_str());
9784        assert_eq!(scene_delta.new_objects, vec![]);
9785        assert_eq!(scene_delta.new_graph.objects.len(), 3);
9786
9787        ctx.close().await;
9788        mock_ctx.close().await;
9789    }
9790
9791    #[tokio::test(flavor = "multi_thread")]
9792    async fn test_delete_coincident_constraint() {
9793        let initial_source = "\
9794sketch(on = XY) {
9795  point1 = point(at = [var 1, var 2])
9796  point2 = point(at = [var 3, var 4])
9797  coincident([point1, point2])
9798  point(at = [var 5, var 6])
9799}
9800";
9801
9802        let program = Program::parse(initial_source).unwrap().0.unwrap();
9803
9804        let mut frontend = FrontendState::new();
9805
9806        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9807        let mock_ctx = ExecutorContext::new_mock(None).await;
9808        let version = Version(0);
9809
9810        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9811        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9812        let sketch_id = sketch_object.id;
9813        let sketch = expect_sketch(sketch_object);
9814
9815        let coincident_id = *sketch.constraints.first().unwrap();
9816
9817        let (src_delta, scene_delta) = frontend
9818            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
9819            .await
9820            .unwrap();
9821        insta::assert_snapshot!("test_delete_coincident_constraint", src_delta.text.as_str());
9822        assert_eq!(scene_delta.new_objects, vec![]);
9823        assert_eq!(scene_delta.new_graph.objects.len(), 5);
9824
9825        ctx.close().await;
9826        mock_ctx.close().await;
9827    }
9828
9829    #[tokio::test(flavor = "multi_thread")]
9830    async fn test_delete_line_cascades_to_coincident_constraint() {
9831        let initial_source = "\
9832sketch(on = XY) {
9833  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9834  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9835  coincident([line1.end, line2.start])
9836}
9837";
9838
9839        let program = Program::parse(initial_source).unwrap().0.unwrap();
9840
9841        let mut frontend = FrontendState::new();
9842
9843        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9844        let mock_ctx = ExecutorContext::new_mock(None).await;
9845        let version = Version(0);
9846
9847        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9848        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9849        let sketch_id = sketch_object.id;
9850        let sketch = expect_sketch(sketch_object);
9851        let line_id = *sketch.segments.get(5).unwrap();
9852
9853        let (src_delta, scene_delta) = frontend
9854            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9855            .await
9856            .unwrap();
9857        insta::assert_snapshot!(
9858            "test_delete_line_cascades_to_coincident_constraint",
9859            src_delta.text.as_str()
9860        );
9861        assert_eq!(
9862            scene_delta.new_graph.objects.len(),
9863            5,
9864            "{:#?}",
9865            scene_delta.new_graph.objects
9866        );
9867
9868        ctx.close().await;
9869        mock_ctx.close().await;
9870    }
9871
9872    #[tokio::test(flavor = "multi_thread")]
9873    async fn test_delete_line_cascades_to_distance_constraint() {
9874        let initial_source = "\
9875sketch(on = XY) {
9876  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9877  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9878  distance([line1.end, line2.start]) == 10mm
9879}
9880";
9881
9882        let program = Program::parse(initial_source).unwrap().0.unwrap();
9883
9884        let mut frontend = FrontendState::new();
9885
9886        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9887        let mock_ctx = ExecutorContext::new_mock(None).await;
9888        let version = Version(0);
9889
9890        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9891        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9892        let sketch_id = sketch_object.id;
9893        let sketch = expect_sketch(sketch_object);
9894        let line_id = *sketch.segments.get(5).unwrap();
9895
9896        let (src_delta, scene_delta) = frontend
9897            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9898            .await
9899            .unwrap();
9900        insta::assert_snapshot!(
9901            "test_delete_line_cascades_to_distance_constraint",
9902            src_delta.text.as_str()
9903        );
9904        assert_eq!(
9905            scene_delta.new_graph.objects.len(),
9906            5,
9907            "{:#?}",
9908            scene_delta.new_graph.objects
9909        );
9910
9911        ctx.close().await;
9912        mock_ctx.close().await;
9913    }
9914
9915    #[tokio::test(flavor = "multi_thread")]
9916    async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
9917        let initial_source = "\
9918sketch(on = XY) {
9919  point1 = point(at = [var 1, var 2])
9920  point2 = point(at = [var 3, var 4])
9921  horizontalDistance([point1, point2]) == 10mm
9922}
9923";
9924
9925        let program = Program::parse(initial_source).unwrap().0.unwrap();
9926
9927        let mut frontend = FrontendState::new();
9928
9929        let mock_ctx = ExecutorContext::new_mock(None).await;
9930        let version = Version(0);
9931
9932        frontend.program = program.clone();
9933        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9934        frontend.update_state_after_exec(outcome, true);
9935        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9936        let sketch_id = sketch_object.id;
9937        let sketch = expect_sketch(sketch_object);
9938        let point2_id = *sketch.segments.get(1).unwrap();
9939
9940        let (src_delta, scene_delta) = frontend
9941            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
9942            .await
9943            .unwrap();
9944        insta::assert_snapshot!(
9945            "test_delete_point_cascades_to_horizontal_distance_constraint",
9946            src_delta.text.as_str()
9947        );
9948        assert_eq!(
9949            scene_delta.new_graph.objects.len(),
9950            3,
9951            "{:#?}",
9952            scene_delta.new_graph.objects
9953        );
9954
9955        mock_ctx.close().await;
9956    }
9957
9958    #[tokio::test(flavor = "multi_thread")]
9959    async fn test_delete_line_cascades_to_fixed_constraint() {
9960        let initial_source = "\
9961sketch(on = XY) {
9962  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9963  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9964  fixed([line1.start, [0, 0]])
9965}
9966";
9967
9968        let program = Program::parse(initial_source).unwrap().0.unwrap();
9969
9970        let mut frontend = FrontendState::new();
9971
9972        let mock_ctx = ExecutorContext::new_mock(None).await;
9973        let version = Version(0);
9974
9975        frontend.program = program.clone();
9976        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9977        frontend.update_state_after_exec(outcome, true);
9978        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9979        let sketch_id = sketch_object.id;
9980        let sketch = expect_sketch(sketch_object);
9981        let line1_id = *sketch.segments.get(2).unwrap();
9982
9983        let (src_delta, scene_delta) = frontend
9984            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9985            .await
9986            .unwrap();
9987        insta::assert_snapshot!("test_delete_line_cascades_to_fixed_constraint", src_delta.text.as_str());
9988        assert_eq!(
9989            scene_delta.new_graph.objects.len(),
9990            5,
9991            "{:#?}",
9992            scene_delta.new_graph.objects
9993        );
9994
9995        mock_ctx.close().await;
9996    }
9997
9998    #[tokio::test(flavor = "multi_thread")]
9999    async fn test_delete_line_cascades_to_midpoint_constraint() {
10000        let initial_source = "\
10001sketch(on = XY) {
10002  point1 = point(at = [var 1, var 2])
10003  line1 = line(start = [var 0, var 0], end = [var 6, var 4])
10004  midpoint(line1, point = point1)
10005}
10006";
10007
10008        let program = Program::parse(initial_source).unwrap().0.unwrap();
10009
10010        let mut frontend = FrontendState::new();
10011
10012        let mock_ctx = ExecutorContext::new_mock(None).await;
10013        let version = Version(0);
10014
10015        frontend.program = program.clone();
10016        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10017        frontend.update_state_after_exec(outcome, true);
10018        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10019        let sketch_id = sketch_object.id;
10020        let sketch = expect_sketch(sketch_object);
10021        let line1_id = *sketch.segments.get(3).unwrap();
10022
10023        let (src_delta, scene_delta) = frontend
10024            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10025            .await
10026            .unwrap();
10027        insta::assert_snapshot!(
10028            "test_delete_line_cascades_to_midpoint_constraint",
10029            src_delta.text.as_str()
10030        );
10031        assert_eq!(
10032            scene_delta.new_graph.objects.len(),
10033            3,
10034            "{:#?}",
10035            scene_delta.new_graph.objects
10036        );
10037
10038        mock_ctx.close().await;
10039    }
10040
10041    #[tokio::test(flavor = "multi_thread")]
10042    async fn test_delete_point_preserves_multiline_coincident_constraint() {
10043        let initial_source = "\
10044sketch(on = XY) {
10045  point1 = point(at = [var 1, var 2])
10046  point2 = point(at = [var 3, var 4])
10047  point3 = point(at = [var 5, var 6])
10048  coincident([point1, point2, point3])
10049}
10050";
10051
10052        let program = Program::parse(initial_source).unwrap().0.unwrap();
10053
10054        let mut frontend = FrontendState::new();
10055
10056        let mock_ctx = ExecutorContext::new_mock(None).await;
10057        let version = Version(0);
10058
10059        frontend.program = program.clone();
10060        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10061        frontend.update_state_after_exec(outcome, true);
10062        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10063        let sketch_id = sketch_object.id;
10064        let sketch = expect_sketch(sketch_object);
10065        let point3_id = *sketch.segments.get(2).unwrap();
10066
10067        let (src_delta, scene_delta) = frontend
10068            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
10069            .await
10070            .unwrap();
10071        assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
10072        assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
10073        assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
10074        assert!(
10075            src_delta.text.contains("coincident([point1, point2])"),
10076            "{}",
10077            src_delta.text
10078        );
10079
10080        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10081        let sketch = expect_sketch(sketch_object);
10082        assert_eq!(sketch.segments.len(), 2);
10083        assert_eq!(sketch.constraints.len(), 1);
10084
10085        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10086        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10087            panic!("Expected constraint object");
10088        };
10089        let Constraint::Coincident(coincident) = constraint else {
10090            panic!("Expected coincident constraint");
10091        };
10092        assert_eq!(
10093            coincident.segments,
10094            sketch
10095                .segments
10096                .iter()
10097                .copied()
10098                .map(Into::into)
10099                .collect::<Vec<ConstraintSegment>>()
10100        );
10101
10102        mock_ctx.close().await;
10103    }
10104
10105    #[tokio::test(flavor = "multi_thread")]
10106    async fn test_delete_line_preserves_multiline_equal_length_constraint() {
10107        let initial_source = "\
10108sketch(on = XY) {
10109  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10110  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10111  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10112  equalLength([line1, line2, line3])
10113}
10114";
10115
10116        let program = Program::parse(initial_source).unwrap().0.unwrap();
10117
10118        let mut frontend = FrontendState::new();
10119
10120        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10121        let mock_ctx = ExecutorContext::new_mock(None).await;
10122        let version = Version(0);
10123
10124        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10125        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10126        let sketch_id = sketch_object.id;
10127        let sketch = expect_sketch(sketch_object);
10128        let line3_id = *sketch.segments.get(8).unwrap();
10129
10130        let (src_delta, scene_delta) = frontend
10131            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10132            .await
10133            .unwrap();
10134        insta::assert_snapshot!(
10135            "test_delete_line_preserves_multiline_equal_length_constraint",
10136            src_delta.text.as_str()
10137        );
10138
10139        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10140        let sketch = expect_sketch(sketch_object);
10141        assert_eq!(sketch.constraints.len(), 1);
10142
10143        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10144        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10145            panic!("Expected constraint object");
10146        };
10147        let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
10148            panic!("Expected lines equal length constraint");
10149        };
10150        assert_eq!(lines_equal_length.lines.len(), 2);
10151
10152        ctx.close().await;
10153        mock_ctx.close().await;
10154    }
10155
10156    #[tokio::test(flavor = "multi_thread")]
10157    async fn test_delete_line_preserves_multiline_horizontal_constraint() {
10158        let initial_source = "\
10159sketch(on = XY) {
10160  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10161  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10162  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10163  horizontal([line1.end, line2.start, line3.start])
10164}
10165";
10166
10167        let program = Program::parse(initial_source).unwrap().0.unwrap();
10168
10169        let mut frontend = FrontendState::new();
10170
10171        let mock_ctx = ExecutorContext::new_mock(None).await;
10172        let version = Version(0);
10173
10174        frontend.program = program.clone();
10175        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10176        frontend.update_state_after_exec(outcome, true);
10177        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10178        let sketch_id = sketch_object.id;
10179        let sketch = expect_sketch(sketch_object);
10180        let line1_id = *sketch.segments.get(2).unwrap();
10181
10182        let (src_delta, scene_delta) = frontend
10183            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10184            .await
10185            .unwrap();
10186        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10187        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10188        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10189        assert!(
10190            src_delta.text.contains("horizontal([line2.start, line3.start])"),
10191            "{}",
10192            src_delta.text
10193        );
10194
10195        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10196        let sketch = expect_sketch(sketch_object);
10197        assert_eq!(sketch.constraints.len(), 1);
10198
10199        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10200        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10201            panic!("Expected constraint object");
10202        };
10203        let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
10204            panic!("Expected horizontal points constraint");
10205        };
10206        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10207        assert_eq!(*points, remaining_points);
10208
10209        mock_ctx.close().await;
10210    }
10211
10212    #[tokio::test(flavor = "multi_thread")]
10213    async fn test_delete_line_preserves_multiline_vertical_constraint() {
10214        let initial_source = "\
10215sketch(on = XY) {
10216  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10217  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10218  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10219  vertical([line1.end, line2.start, line3.start])
10220}
10221";
10222
10223        let program = Program::parse(initial_source).unwrap().0.unwrap();
10224
10225        let mut frontend = FrontendState::new();
10226
10227        let mock_ctx = ExecutorContext::new_mock(None).await;
10228        let version = Version(0);
10229
10230        frontend.program = program.clone();
10231        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10232        frontend.update_state_after_exec(outcome, true);
10233        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10234        let sketch_id = sketch_object.id;
10235        let sketch = expect_sketch(sketch_object);
10236        let line1_id = *sketch.segments.get(2).unwrap();
10237
10238        let (src_delta, scene_delta) = frontend
10239            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10240            .await
10241            .unwrap();
10242        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10243        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10244        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10245        assert!(
10246            src_delta.text.contains("vertical([line2.start, line3.start])"),
10247            "{}",
10248            src_delta.text
10249        );
10250
10251        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10252        let sketch = expect_sketch(sketch_object);
10253        assert_eq!(sketch.constraints.len(), 1);
10254
10255        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10256        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10257            panic!("Expected constraint object");
10258        };
10259        let Constraint::Vertical(Vertical::Points { points }) = constraint else {
10260            panic!("Expected vertical points constraint");
10261        };
10262        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10263        assert_eq!(*points, remaining_points);
10264
10265        mock_ctx.close().await;
10266    }
10267
10268    #[tokio::test(flavor = "multi_thread")]
10269    async fn test_delete_line_preserves_multiline_coincident_constraint() {
10270        let initial_source = "\
10271sketch(on = XY) {
10272  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10273  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10274  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10275  coincident([line1.end, line2.start, line3.start])
10276}
10277";
10278
10279        let program = Program::parse(initial_source).unwrap().0.unwrap();
10280
10281        let mut frontend = FrontendState::new();
10282
10283        let mock_ctx = ExecutorContext::new_mock(None).await;
10284        let version = Version(0);
10285
10286        frontend.program = program.clone();
10287        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10288        frontend.update_state_after_exec(outcome, true);
10289        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10290        let sketch_id = sketch_object.id;
10291        let sketch = expect_sketch(sketch_object);
10292        let line1_id = *sketch.segments.get(2).unwrap();
10293
10294        let (src_delta, scene_delta) = frontend
10295            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10296            .await
10297            .unwrap();
10298        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10299        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10300        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10301        assert!(
10302            src_delta.text.contains("coincident([line2.start, line3.start])"),
10303            "{}",
10304            src_delta.text
10305        );
10306
10307        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10308        let sketch = expect_sketch(sketch_object);
10309        assert_eq!(sketch.constraints.len(), 1);
10310
10311        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10312        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10313            panic!("Expected constraint object");
10314        };
10315        let Constraint::Coincident(coincident) = constraint else {
10316            panic!("Expected coincident constraint");
10317        };
10318        let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10319        assert_eq!(coincident.segments, remaining_segments);
10320
10321        mock_ctx.close().await;
10322    }
10323
10324    #[tokio::test(flavor = "multi_thread")]
10325    async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
10326        let initial_source = "\
10327sketch(on = XY) {
10328  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10329  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10330  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10331  equalLength([line1, line2, line3])
10332}
10333";
10334
10335        let program = Program::parse(initial_source).unwrap().0.unwrap();
10336
10337        let mut frontend = FrontendState::new();
10338
10339        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10340        let mock_ctx = ExecutorContext::new_mock(None).await;
10341        let version = Version(0);
10342
10343        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10344        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10345        let sketch_id = sketch_object.id;
10346        let sketch = expect_sketch(sketch_object);
10347        let line2_id = *sketch.segments.get(5).unwrap();
10348        let line3_id = *sketch.segments.get(8).unwrap();
10349
10350        let (src_delta, scene_delta) = frontend
10351            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10352            .await
10353            .unwrap();
10354        insta::assert_snapshot!(
10355            "test_delete_lines_removes_multiline_equal_length_constraint_below_minimum",
10356            src_delta.text.as_str()
10357        );
10358
10359        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10360        let sketch = expect_sketch(sketch_object);
10361        assert!(sketch.constraints.is_empty());
10362
10363        ctx.close().await;
10364        mock_ctx.close().await;
10365    }
10366
10367    #[tokio::test(flavor = "multi_thread")]
10368    async fn test_delete_line_preserves_multiline_parallel_constraint() {
10369        let initial_source = "\
10370sketch(on = XY) {
10371  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10372  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10373  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10374  parallel([line1, line2, line3])
10375}
10376";
10377
10378        let program = Program::parse(initial_source).unwrap().0.unwrap();
10379
10380        let mut frontend = FrontendState::new();
10381
10382        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10383        let mock_ctx = ExecutorContext::new_mock(None).await;
10384        let version = Version(0);
10385
10386        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10387        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10388        let sketch_id = sketch_object.id;
10389        let sketch = expect_sketch(sketch_object);
10390        let line3_id = *sketch.segments.get(8).unwrap();
10391
10392        let (src_delta, scene_delta) = frontend
10393            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10394            .await
10395            .unwrap();
10396        insta::assert_snapshot!(
10397            "test_delete_line_preserves_multiline_parallel_constraint",
10398            src_delta.text.as_str()
10399        );
10400
10401        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10402        let sketch = expect_sketch(sketch_object);
10403        assert_eq!(sketch.constraints.len(), 1);
10404
10405        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10406        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10407            panic!("Expected constraint object");
10408        };
10409        let Constraint::Parallel(parallel) = constraint else {
10410            panic!("Expected parallel constraint");
10411        };
10412        assert_eq!(parallel.lines.len(), 2);
10413
10414        ctx.close().await;
10415        mock_ctx.close().await;
10416    }
10417
10418    #[tokio::test(flavor = "multi_thread")]
10419    async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
10420        let initial_source = "\
10421sketch(on = XY) {
10422  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10423  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10424  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10425  parallel([line1, line2, line3])
10426}
10427";
10428
10429        let program = Program::parse(initial_source).unwrap().0.unwrap();
10430
10431        let mut frontend = FrontendState::new();
10432
10433        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10434        let mock_ctx = ExecutorContext::new_mock(None).await;
10435        let version = Version(0);
10436
10437        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10438        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10439        let sketch_id = sketch_object.id;
10440        let sketch = expect_sketch(sketch_object);
10441        let line2_id = *sketch.segments.get(5).unwrap();
10442        let line3_id = *sketch.segments.get(8).unwrap();
10443
10444        let (src_delta, scene_delta) = frontend
10445            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10446            .await
10447            .unwrap();
10448        insta::assert_snapshot!(
10449            "test_delete_lines_removes_multiline_parallel_constraint_below_minimum",
10450            src_delta.text.as_str()
10451        );
10452
10453        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10454        let sketch = expect_sketch(sketch_object);
10455        assert!(sketch.constraints.is_empty());
10456
10457        ctx.close().await;
10458        mock_ctx.close().await;
10459    }
10460
10461    #[tokio::test(flavor = "multi_thread")]
10462    async fn test_delete_line_line_coincident_constraint() {
10463        let initial_source = "\
10464sketch(on = XY) {
10465  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10466  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10467  coincident([line1, line2])
10468}
10469";
10470
10471        let program = Program::parse(initial_source).unwrap().0.unwrap();
10472
10473        let mut frontend = FrontendState::new();
10474
10475        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10476        let mock_ctx = ExecutorContext::new_mock(None).await;
10477        let version = Version(0);
10478
10479        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10480        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10481        let sketch_id = sketch_object.id;
10482        let sketch = expect_sketch(sketch_object);
10483
10484        let coincident_id = *sketch.constraints.first().unwrap();
10485
10486        let (src_delta, scene_delta) = frontend
10487            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
10488            .await
10489            .unwrap();
10490        insta::assert_snapshot!("test_delete_line_line_coincident_constraint", src_delta.text.as_str());
10491        assert_eq!(scene_delta.new_objects, vec![]);
10492        assert_eq!(scene_delta.new_graph.objects.len(), 8);
10493
10494        ctx.close().await;
10495        mock_ctx.close().await;
10496    }
10497
10498    #[tokio::test(flavor = "multi_thread")]
10499    async fn test_two_points_coincident() {
10500        let initial_source = "\
10501sketch(on = XY) {
10502  point1 = point(at = [var 1, var 2])
10503  point(at = [3, 4])
10504}
10505";
10506
10507        let program = Program::parse(initial_source).unwrap().0.unwrap();
10508
10509        let mut frontend = FrontendState::new();
10510
10511        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10512        let mock_ctx = ExecutorContext::new_mock(None).await;
10513        let version = Version(0);
10514
10515        frontend.hack_set_program(&ctx, program).await.unwrap();
10516        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10517        let sketch_id = sketch_object.id;
10518        let sketch = expect_sketch(sketch_object);
10519        let point0_id = *sketch.segments.first().unwrap();
10520        let point1_id = *sketch.segments.get(1).unwrap();
10521
10522        let constraint = Constraint::Coincident(Coincident {
10523            segments: vec![point0_id.into(), point1_id.into()],
10524        });
10525        let (src_delta, scene_delta) = frontend
10526            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10527            .await
10528            .unwrap();
10529        insta::assert_snapshot!("test_two_points_coincident", src_delta.text.as_str());
10530        assert_eq!(
10531            scene_delta.new_graph.objects.len(),
10532            5,
10533            "{:#?}",
10534            scene_delta.new_graph.objects
10535        );
10536
10537        ctx.close().await;
10538        mock_ctx.close().await;
10539    }
10540
10541    #[tokio::test(flavor = "multi_thread")]
10542    async fn test_three_points_coincident() {
10543        let initial_source = "\
10544sketch(on = XY) {
10545  point1 = point(at = [var 1, var 2])
10546  point(at = [var 3, var 4])
10547  point(at = [var 5, var 6])
10548}
10549";
10550
10551        let program = Program::parse(initial_source).unwrap().0.unwrap();
10552
10553        let mut frontend = FrontendState::new();
10554
10555        let mock_ctx = ExecutorContext::new_mock(None).await;
10556        let version = Version(0);
10557
10558        frontend.program = program.clone();
10559        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10560        frontend.update_state_after_exec(outcome, true);
10561        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10562        let sketch_id = sketch_object.id;
10563        let sketch = expect_sketch(sketch_object);
10564        let segments = sketch
10565            .segments
10566            .iter()
10567            .take(3)
10568            .copied()
10569            .map(Into::into)
10570            .collect::<Vec<ConstraintSegment>>();
10571
10572        let constraint = Constraint::Coincident(Coincident {
10573            segments: segments.clone(),
10574        });
10575        let (src_delta, scene_delta) = frontend
10576            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10577            .await
10578            .unwrap();
10579        insta::assert_snapshot!("test_three_points_coincident", src_delta.text.as_str());
10580
10581        let constraint_object = scene_delta
10582            .new_graph
10583            .objects
10584            .iter()
10585            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10586            .unwrap();
10587
10588        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10589            panic!("expected a constraint object");
10590        };
10591
10592        assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10593
10594        mock_ctx.close().await;
10595    }
10596
10597    #[tokio::test(flavor = "multi_thread")]
10598    async fn test_source_with_three_point_coincident_tracks_all_segments() {
10599        let initial_source = "\
10600sketch(on = XY) {
10601  point1 = point(at = [var 1, var 2])
10602  point2 = point(at = [var 3, var 4])
10603  point3 = point(at = [var 5, var 6])
10604  coincident([point1, point2, point3])
10605}
10606";
10607
10608        let program = Program::parse(initial_source).unwrap().0.unwrap();
10609
10610        let mut frontend = FrontendState::new();
10611
10612        let ctx = ExecutorContext::new_mock(None).await;
10613        frontend.program = program.clone();
10614        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10615        frontend.update_state_after_exec(outcome, true);
10616
10617        let constraint_object = frontend
10618            .scene_graph
10619            .objects
10620            .iter()
10621            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10622            .unwrap();
10623        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10624            panic!("expected a constraint object");
10625        };
10626
10627        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10628        let sketch = expect_sketch(sketch_object);
10629        let expected_segments = sketch
10630            .segments
10631            .iter()
10632            .take(3)
10633            .copied()
10634            .map(Into::into)
10635            .collect::<Vec<ConstraintSegment>>();
10636
10637        assert_eq!(
10638            constraint,
10639            &Constraint::Coincident(Coincident {
10640                segments: expected_segments,
10641            })
10642        );
10643
10644        ctx.close().await;
10645    }
10646
10647    #[tokio::test(flavor = "multi_thread")]
10648    async fn test_point_origin_coincident_preserves_order() {
10649        let initial_source = "\
10650sketch(on = XY) {
10651  point(at = [var 1, var 2])
10652}
10653";
10654
10655        for (origin_first, snapshot_name) in [
10656            (true, "test_point_origin_coincident_preserves_order_origin_first"),
10657            (false, "test_point_origin_coincident_preserves_order_point_first"),
10658        ] {
10659            let program = Program::parse(initial_source).unwrap().0.unwrap();
10660
10661            let mut frontend = FrontendState::new();
10662
10663            let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10664            let mock_ctx = ExecutorContext::new_mock(None).await;
10665            let version = Version(0);
10666
10667            frontend.hack_set_program(&ctx, program).await.unwrap();
10668            let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10669            let sketch_id = sketch_object.id;
10670            let sketch = expect_sketch(sketch_object);
10671            let point_id = *sketch.segments.first().unwrap();
10672
10673            let segments = if origin_first {
10674                vec![ConstraintSegment::ORIGIN, point_id.into()]
10675            } else {
10676                vec![point_id.into(), ConstraintSegment::ORIGIN]
10677            };
10678            let constraint = Constraint::Coincident(Coincident {
10679                segments: segments.clone(),
10680            });
10681            let (src_delta, scene_delta) = frontend
10682                .add_constraint(&mock_ctx, version, sketch_id, constraint)
10683                .await
10684                .unwrap();
10685            insta::assert_snapshot!(snapshot_name, src_delta.text.as_str());
10686
10687            let constraint_object = scene_delta
10688                .new_graph
10689                .objects
10690                .iter()
10691                .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10692                .unwrap();
10693
10694            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10695                panic!("expected a constraint object");
10696            };
10697
10698            assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10699
10700            ctx.close().await;
10701            mock_ctx.close().await;
10702        }
10703    }
10704
10705    #[tokio::test(flavor = "multi_thread")]
10706    async fn test_coincident_of_line_end_points() {
10707        let initial_source = "\
10708sketch(on = XY) {
10709  line(start = [var 1, var 2], end = [var 3, var 4])
10710  line(start = [var 5, var 6], end = [var 7, var 8])
10711}
10712";
10713
10714        let program = Program::parse(initial_source).unwrap().0.unwrap();
10715
10716        let mut frontend = FrontendState::new();
10717
10718        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10719        let mock_ctx = ExecutorContext::new_mock(None).await;
10720        let version = Version(0);
10721
10722        frontend.hack_set_program(&ctx, program).await.unwrap();
10723        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10724        let sketch_id = sketch_object.id;
10725        let sketch = expect_sketch(sketch_object);
10726        let point0_id = *sketch.segments.get(1).unwrap();
10727        let point1_id = *sketch.segments.get(3).unwrap();
10728
10729        let constraint = Constraint::Coincident(Coincident {
10730            segments: vec![point0_id.into(), point1_id.into()],
10731        });
10732        let (src_delta, scene_delta) = frontend
10733            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10734            .await
10735            .unwrap();
10736        insta::assert_snapshot!("test_coincident_of_line_end_points", src_delta.text.as_str());
10737        assert_eq!(
10738            scene_delta.new_graph.objects.len(),
10739            9,
10740            "{:#?}",
10741            scene_delta.new_graph.objects
10742        );
10743
10744        ctx.close().await;
10745        mock_ctx.close().await;
10746    }
10747
10748    #[tokio::test(flavor = "multi_thread")]
10749    async fn test_coincident_of_line_point_and_circle_segment() {
10750        let initial_source = "\
10751sketch(on = XY) {
10752  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
10753  line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
10754}
10755";
10756        let program = Program::parse(initial_source).unwrap().0.unwrap();
10757        let mut frontend = FrontendState::new();
10758
10759        let mock_ctx = ExecutorContext::new_mock(None).await;
10760        let version = Version(0);
10761
10762        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10763        frontend.program = program;
10764        frontend.update_state_after_exec(outcome, true);
10765        let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
10766        let sketch_id = sketch_object.id;
10767        let sketch = expect_sketch(sketch_object);
10768
10769        let circle_id = sketch
10770            .segments
10771            .iter()
10772            .copied()
10773            .find(|seg_id| {
10774                matches!(
10775                    &frontend.scene_graph.objects[seg_id.0].kind,
10776                    ObjectKind::Segment {
10777                        segment: Segment::Circle(_)
10778                    }
10779                )
10780            })
10781            .expect("Expected a circle segment in sketch");
10782        let line_id = frontend
10783            .scene_graph
10784            .objects
10785            .iter()
10786            .find_map(|obj| match &obj.kind {
10787                ObjectKind::Segment {
10788                    segment: Segment::Line(line),
10789                } if line.owner.is_none() => Some(obj.id),
10790                _ => None,
10791            })
10792            .expect("Expected a standalone line segment in scene graph");
10793
10794        let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
10795            ObjectKind::Segment {
10796                segment: Segment::Line(line),
10797            } => line.start,
10798            _ => panic!("Expected line segment object"),
10799        };
10800
10801        let constraint = Constraint::Coincident(Coincident {
10802            segments: vec![line_start_point_id.into(), circle_id.into()],
10803        });
10804        let (src_delta, _scene_delta) = frontend
10805            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10806            .await
10807            .unwrap();
10808        insta::assert_snapshot!(
10809            "test_coincident_of_line_point_and_circle_segment",
10810            src_delta.text.as_str()
10811        );
10812
10813        mock_ctx.close().await;
10814    }
10815
10816    #[tokio::test(flavor = "multi_thread")]
10817    async fn test_invalid_coincident_arc_and_line_preserves_state() {
10818        // Test that attempting an invalid coincident constraint (arc and line)
10819        // doesn't corrupt the state, allowing subsequent operations to work.
10820        // This test verifies the transactional fix in add_constraint that prevents
10821        // state corruption when invalid constraints are attempted.
10822        // Example: coincident constraint between an arc segment and a straight line segment
10823        // is geometrically invalid and should fail, but state should remain intact.
10824        // Use the programmatic approach (new_sketch + add_segment) like test_new_sketch_add_arc_edit_arc
10825        let program = Program::empty();
10826
10827        let mut frontend = FrontendState::new();
10828        frontend.program = program;
10829
10830        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10831        let mock_ctx = ExecutorContext::new_mock(None).await;
10832        let version = Version(0);
10833
10834        let sketch_args = SketchCtor {
10835            on: Plane::Default(PlaneName::Xy),
10836        };
10837        let (_src_delta, _scene_delta, sketch_id) = frontend
10838            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10839            .await
10840            .unwrap();
10841
10842        // Add an arc segment
10843        let arc_ctor = ArcCtor {
10844            start: Point2d {
10845                x: Expr::Var(Number {
10846                    value: 0.0,
10847                    units: NumericSuffix::Mm,
10848                }),
10849                y: Expr::Var(Number {
10850                    value: 0.0,
10851                    units: NumericSuffix::Mm,
10852                }),
10853            },
10854            end: Point2d {
10855                x: Expr::Var(Number {
10856                    value: 10.0,
10857                    units: NumericSuffix::Mm,
10858                }),
10859                y: Expr::Var(Number {
10860                    value: 10.0,
10861                    units: NumericSuffix::Mm,
10862                }),
10863            },
10864            center: Point2d {
10865                x: Expr::Var(Number {
10866                    value: 10.0,
10867                    units: NumericSuffix::Mm,
10868                }),
10869                y: Expr::Var(Number {
10870                    value: 0.0,
10871                    units: NumericSuffix::Mm,
10872                }),
10873            },
10874            direction: None,
10875            construction: None,
10876        };
10877        let (_src_delta, scene_delta) = frontend
10878            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
10879            .await
10880            .unwrap();
10881        // The arc is the last object in new_objects (after the 3 points: start, end, center)
10882        let arc_id = *scene_delta.new_objects.last().unwrap();
10883
10884        // Add a line segment
10885        let line_ctor = LineCtor {
10886            start: Point2d {
10887                x: Expr::Var(Number {
10888                    value: 20.0,
10889                    units: NumericSuffix::Mm,
10890                }),
10891                y: Expr::Var(Number {
10892                    value: 0.0,
10893                    units: NumericSuffix::Mm,
10894                }),
10895            },
10896            end: Point2d {
10897                x: Expr::Var(Number {
10898                    value: 30.0,
10899                    units: NumericSuffix::Mm,
10900                }),
10901                y: Expr::Var(Number {
10902                    value: 10.0,
10903                    units: NumericSuffix::Mm,
10904                }),
10905            },
10906            construction: None,
10907        };
10908        let (_src_delta, scene_delta) = frontend
10909            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
10910            .await
10911            .unwrap();
10912        // The line is the last object in new_objects (after the 2 points: start, end)
10913        let line_id = *scene_delta.new_objects.last().unwrap();
10914
10915        // Attempt to add an invalid coincident constraint between arc and line
10916        // This should fail during execution, but state should remain intact
10917        let constraint = Constraint::Coincident(Coincident {
10918            segments: vec![arc_id.into(), line_id.into()],
10919        });
10920        let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
10921
10922        // The constraint addition should fail (invalid constraint)
10923        assert!(result.is_err(), "Expected invalid coincident constraint to fail");
10924
10925        // Verify state is not corrupted by checking that we can still access the scene graph
10926        // and that the original segments are still present with their source ranges
10927        let sketch_object_after =
10928            find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
10929        let sketch_after = expect_sketch(sketch_object_after);
10930
10931        // Verify both segments are still in the sketch
10932        assert!(
10933            sketch_after.segments.contains(&arc_id),
10934            "Arc segment should still exist after failed constraint"
10935        );
10936        assert!(
10937            sketch_after.segments.contains(&line_id),
10938            "Line segment should still exist after failed constraint"
10939        );
10940
10941        // Verify we can still access segment objects (this would fail if source ranges were corrupted)
10942        let arc_obj = frontend
10943            .scene_graph
10944            .objects
10945            .get(arc_id.0)
10946            .expect("Arc object should still be accessible");
10947        let line_obj = frontend
10948            .scene_graph
10949            .objects
10950            .get(line_id.0)
10951            .expect("Line object should still be accessible");
10952
10953        // Verify source ranges are still valid (not corrupted)
10954        // Just verify that the objects are still accessible and have the expected types
10955        match &arc_obj.kind {
10956            ObjectKind::Segment {
10957                segment: Segment::Arc(_),
10958            } => {}
10959            _ => panic!("Arc object should still be an arc segment"),
10960        }
10961        match &line_obj.kind {
10962            ObjectKind::Segment {
10963                segment: Segment::Line(_),
10964            } => {}
10965            _ => panic!("Line object should still be a line segment"),
10966        }
10967
10968        ctx.close().await;
10969        mock_ctx.close().await;
10970    }
10971
10972    #[tokio::test(flavor = "multi_thread")]
10973    async fn test_distance_two_points() {
10974        let initial_source = "\
10975sketch(on = XY) {
10976  point(at = [var 1, var 2])
10977  point(at = [var 3, var 4])
10978}
10979";
10980
10981        let program = Program::parse(initial_source).unwrap().0.unwrap();
10982
10983        let mut frontend = FrontendState::new();
10984
10985        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10986        let mock_ctx = ExecutorContext::new_mock(None).await;
10987        let version = Version(0);
10988
10989        frontend.hack_set_program(&ctx, program).await.unwrap();
10990        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10991        let sketch_id = sketch_object.id;
10992        let sketch = expect_sketch(sketch_object);
10993        let point0_id = *sketch.segments.first().unwrap();
10994        let point1_id = *sketch.segments.get(1).unwrap();
10995
10996        let constraint = Constraint::Distance(Distance {
10997            segments: vec![point0_id.into(), point1_id.into()],
10998            distance: Number {
10999                value: 2.0,
11000                units: NumericSuffix::Mm,
11001            },
11002            label_position: None,
11003            source: Default::default(),
11004        });
11005        let (src_delta, scene_delta) = frontend
11006            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11007            .await
11008            .unwrap();
11009        insta::assert_snapshot!("test_distance_two_points", src_delta.text.as_str());
11010        assert_eq!(
11011            scene_delta.new_graph.objects.len(),
11012            5,
11013            "{:#?}",
11014            scene_delta.new_graph.objects
11015        );
11016
11017        ctx.close().await;
11018        mock_ctx.close().await;
11019    }
11020
11021    #[tokio::test(flavor = "multi_thread")]
11022    async fn test_distance_two_points_with_label() {
11023        let initial_source = "\
11024sketch(on = XY) {
11025  point(at = [var 1, var 2])
11026  point(at = [var 3, var 4])
11027}
11028";
11029
11030        let program = Program::parse(initial_source).unwrap().0.unwrap();
11031
11032        let mut frontend = FrontendState::new();
11033
11034        let mock_ctx = ExecutorContext::new_mock(None).await;
11035        let version = Version(0);
11036
11037        frontend.program = program.clone();
11038        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11039        frontend.update_state_after_exec(outcome, true);
11040        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11041        let sketch_id = sketch_object.id;
11042        let sketch = expect_sketch(sketch_object);
11043        let point0_id = *sketch.segments.first().unwrap();
11044        let point1_id = *sketch.segments.get(1).unwrap();
11045
11046        let label_position = Point2d {
11047            x: Number {
11048                value: 10.0,
11049                units: NumericSuffix::Mm,
11050            },
11051            y: Number {
11052                value: 11.0,
11053                units: NumericSuffix::Mm,
11054            },
11055        };
11056        let constraint = Constraint::Distance(Distance {
11057            segments: vec![point0_id.into(), point1_id.into()],
11058            distance: Number {
11059                value: 2.0,
11060                units: NumericSuffix::Mm,
11061            },
11062            label_position: Some(label_position.clone()),
11063            source: Default::default(),
11064        });
11065        let (src_delta, scene_delta) = frontend
11066            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11067            .await
11068            .unwrap();
11069        insta::assert_snapshot!("test_distance_two_points_with_label", src_delta.text.as_str());
11070
11071        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11072        let sketch = expect_sketch(sketch_object);
11073        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11074        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11075            panic!("Expected constraint object");
11076        };
11077        let Constraint::Distance(distance) = constraint else {
11078            panic!("Expected distance constraint");
11079        };
11080        assert_eq!(distance.label_position, Some(label_position));
11081
11082        mock_ctx.close().await;
11083    }
11084
11085    #[tokio::test(flavor = "multi_thread")]
11086    async fn test_edit_distance_constraint_label_position() {
11087        let initial_source = "\
11088sketch(on = XY) {
11089  point(at = [var 1, var 2])
11090  point(at = [var 3, var 2])
11091}
11092";
11093
11094        let program = Program::parse(initial_source).unwrap().0.unwrap();
11095
11096        let mut frontend = FrontendState::new();
11097
11098        let mock_ctx = ExecutorContext::new_mock(None).await;
11099        let version = Version(0);
11100
11101        frontend.program = program.clone();
11102        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11103        frontend.update_state_after_exec(outcome, true);
11104        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11105        let sketch_id = sketch_object.id;
11106        let sketch = expect_sketch(sketch_object);
11107        let point0_id = *sketch.segments.first().unwrap();
11108        let point1_id = *sketch.segments.get(1).unwrap();
11109
11110        let constraint = Constraint::Distance(Distance {
11111            segments: vec![point0_id.into(), point1_id.into()],
11112            distance: Number {
11113                value: 2.0,
11114                units: NumericSuffix::Mm,
11115            },
11116            label_position: None,
11117            source: Default::default(),
11118        });
11119        let (_, scene_delta) = frontend
11120            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11121            .await
11122            .unwrap();
11123        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11124        let sketch = expect_sketch(sketch_object);
11125        let constraint_id = sketch.constraints[0];
11126        let label_position = Point2d {
11127            x: Number {
11128                value: 10.0,
11129                units: NumericSuffix::Mm,
11130            },
11131            y: Number {
11132                value: 11.0,
11133                units: NumericSuffix::Mm,
11134            },
11135        };
11136
11137        let (src_delta, scene_delta) = frontend
11138            .edit_distance_constraint_label_position(
11139                &mock_ctx,
11140                version,
11141                sketch_id,
11142                constraint_id,
11143                label_position.clone(),
11144                vec![],
11145            )
11146            .await
11147            .unwrap();
11148        insta::assert_snapshot!("test_edit_distance_constraint_label_position", src_delta.text.as_str());
11149
11150        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11151        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11152            panic!("Expected constraint object");
11153        };
11154        let Constraint::Distance(distance) = constraint else {
11155            panic!("Expected distance constraint");
11156        };
11157        assert_eq!(distance.label_position, Some(label_position));
11158
11159        mock_ctx.close().await;
11160    }
11161
11162    #[tokio::test(flavor = "multi_thread")]
11163    async fn test_edit_distance_constraint_type_and_value() {
11164        let initial_source = "\
11165sketch(on = XY) {
11166  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 3mm])
11167  distance([line1.start, line1.end]) == 5mm
11168}
11169";
11170
11171        let program = Program::parse(initial_source).unwrap().0.unwrap();
11172        let mut frontend = FrontendState::new();
11173        let mock_ctx = ExecutorContext::new_mock(None).await;
11174        let version = Version(0);
11175
11176        frontend.program = program.clone();
11177        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11178        frontend.update_state_after_exec(outcome, true);
11179        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11180        let sketch_id = sketch_object.id;
11181        let sketch = expect_sketch(sketch_object);
11182        let constraint_id = sketch.constraints[0];
11183        let point0_id = sketch.segments[0];
11184        let point1_id = sketch.segments[1];
11185        let label_position = Point2d {
11186            x: Number {
11187                value: 2.0,
11188                units: NumericSuffix::Mm,
11189            },
11190            y: Number {
11191                value: 5.0,
11192                units: NumericSuffix::Mm,
11193            },
11194        };
11195
11196        let (source_delta, scene_delta) = frontend
11197            .edit_distance_constraint_with_options(
11198                &mock_ctx,
11199                version,
11200                sketch_id,
11201                constraint_id,
11202                Constraint::HorizontalDistance(Distance {
11203                    segments: vec![point0_id.into(), point1_id.into()],
11204                    distance: Number {
11205                        value: 4.0,
11206                        units: NumericSuffix::Mm,
11207                    },
11208                    label_position: Some(label_position.clone()),
11209                    source: Default::default(),
11210                }),
11211                EditConstraintOptions {
11212                    commit_solved_initial_guesses: false,
11213                },
11214            )
11215            .await
11216            .unwrap();
11217        assert_eq!(
11218            source_delta.text,
11219            "\
11220sketch(on = XY) {
11221  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 3mm])
11222  horizontalDistance([line1.start, line1.end], labelPosition = [2mm, 5mm]) == 4mm
11223}
11224"
11225        );
11226
11227        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11228        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11229            panic!("Expected constraint object");
11230        };
11231        let Constraint::HorizontalDistance(distance) = constraint else {
11232            panic!("Expected horizontal distance constraint");
11233        };
11234        assert_eq!(distance.distance.value, 4.0);
11235        assert_eq!(distance.label_position, Some(label_position));
11236
11237        mock_ctx.close().await;
11238    }
11239
11240    #[tokio::test(flavor = "multi_thread")]
11241    async fn test_edit_angle_constraint_label_position() {
11242        let initial_source = "\
11243sketch(on = XY) {
11244  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11245  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11246  angle([line1, line2]) == 60deg
11247}
11248";
11249
11250        let program = Program::parse(initial_source).unwrap().0.unwrap();
11251        let mut frontend = FrontendState::new();
11252        let mock_ctx = ExecutorContext::new_mock(None).await;
11253        let version = Version(0);
11254
11255        frontend.program = program.clone();
11256        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11257        frontend.update_state_after_exec(outcome, true);
11258        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11259        let sketch_id = sketch_object.id;
11260        let sketch = expect_sketch(sketch_object);
11261        let constraint_id = sketch.constraints[0];
11262        let label_position = Point2d {
11263            x: Number {
11264                value: 10.0,
11265                units: NumericSuffix::Mm,
11266            },
11267            y: Number {
11268                value: 11.0,
11269                units: NumericSuffix::Mm,
11270            },
11271        };
11272
11273        let (src_delta, scene_delta) = frontend
11274            .edit_distance_constraint_label_position(
11275                &mock_ctx,
11276                version,
11277                sketch_id,
11278                constraint_id,
11279                label_position.clone(),
11280                vec![],
11281            )
11282            .await
11283            .unwrap();
11284        assert_eq!(
11285            src_delta.text.as_str(),
11286            "\
11287sketch(on = XY) {
11288  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11289  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.46mm])
11290  angle([line1, line2], labelPosition = [10mm, 11mm]) == 60deg
11291}
11292"
11293        );
11294
11295        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11296        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11297            panic!("Expected constraint object");
11298        };
11299        let Constraint::Angle(angle) = constraint else {
11300            panic!("Expected angle constraint");
11301        };
11302        assert_eq!(angle.label_position, Some(label_position));
11303
11304        mock_ctx.close().await;
11305    }
11306
11307    #[tokio::test(flavor = "multi_thread")]
11308    async fn test_edit_angle_constraint_label_position_with_call_on_right() {
11309        let initial_source = "\
11310sketch(on = XY) {
11311  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11312  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11313  60deg == angleDimension(lines = [line1, line2], sector = 1)
11314}
11315";
11316
11317        let program = Program::parse(initial_source).unwrap().0.unwrap();
11318        let mut frontend = FrontendState::new();
11319        let mock_ctx = ExecutorContext::new_mock(None).await;
11320        let version = Version(0);
11321
11322        frontend.program = program.clone();
11323        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11324        frontend.update_state_after_exec(outcome, true);
11325        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11326        let sketch_id = sketch_object.id;
11327        let sketch = expect_sketch(sketch_object);
11328        let constraint_id = sketch.constraints[0];
11329        let label_position = Point2d {
11330            x: Number {
11331                value: 10.0,
11332                units: NumericSuffix::Mm,
11333            },
11334            y: Number {
11335                value: 11.0,
11336                units: NumericSuffix::Mm,
11337            },
11338        };
11339
11340        let (src_delta, scene_delta) = frontend
11341            .edit_distance_constraint_label_position(
11342                &mock_ctx,
11343                version,
11344                sketch_id,
11345                constraint_id,
11346                label_position.clone(),
11347                vec![],
11348            )
11349            .await
11350            .unwrap();
11351        assert_eq!(
11352            src_delta.text.as_str(),
11353            "\
11354sketch(on = XY) {
11355  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11356  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.46mm])
11357  60deg == angleDimension(lines = [line1, line2], sector = 1, labelPosition = [10mm, 11mm])
11358}
11359"
11360        );
11361
11362        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11363        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11364            panic!("Expected constraint object");
11365        };
11366        let Constraint::Angle(angle) = constraint else {
11367            panic!("Expected angle constraint");
11368        };
11369        assert_eq!(angle.label_position, Some(label_position));
11370
11371        mock_ctx.close().await;
11372    }
11373
11374    #[tokio::test(flavor = "multi_thread")]
11375    async fn test_edit_angle_constraint() {
11376        let initial_source = "\
11377sketch(on = XY) {
11378  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11379  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11380  angle([line1, line2]) == 60deg
11381}
11382";
11383
11384        let program = Program::parse(initial_source).unwrap().0.unwrap();
11385        let mut frontend = FrontendState::new();
11386        let mock_ctx = ExecutorContext::new_mock(None).await;
11387        let version = Version(0);
11388
11389        frontend.program = program.clone();
11390        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11391        frontend.update_state_after_exec(outcome, true);
11392        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11393        let sketch_id = sketch_object.id;
11394        let sketch = expect_sketch(sketch_object);
11395        let constraint_id = sketch.constraints[0];
11396        let line1_id = *sketch.segments.get(2).unwrap();
11397        let line2_id = *sketch.segments.get(5).unwrap();
11398        let label_position = Point2d {
11399            x: Number {
11400                value: 10.0,
11401                units: NumericSuffix::Mm,
11402            },
11403            y: Number {
11404                value: 11.0,
11405                units: NumericSuffix::Mm,
11406            },
11407        };
11408
11409        let (src_delta, scene_delta) = frontend
11410            .edit_angle_constraint_with_options(
11411                &mock_ctx,
11412                version,
11413                sketch_id,
11414                constraint_id,
11415                Angle {
11416                    lines: vec![line2_id, line1_id],
11417                    angle: Number {
11418                        value: 60.0,
11419                        units: NumericSuffix::Deg,
11420                    },
11421                    sector: Some(3),
11422                    inverse: Some(false),
11423                    label_position: Some(label_position.clone()),
11424                    source: Default::default(),
11425                },
11426                EditConstraintOptions {
11427                    commit_solved_initial_guesses: false,
11428                },
11429            )
11430            .await
11431            .unwrap();
11432        assert_eq!(
11433            src_delta.text.as_str(),
11434            "\
11435sketch(on = XY) {
11436  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11437  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11438  angleDimension(lines = [line2, line1], sector = 3, labelPosition = [10mm, 11mm]) == 60deg
11439}
11440"
11441        );
11442
11443        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11444        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11445            panic!("Expected constraint object");
11446        };
11447        let Constraint::Angle(angle) = constraint else {
11448            panic!("Expected angle constraint");
11449        };
11450        assert_eq!(angle.lines, vec![line2_id, line1_id]);
11451        assert_eq!(angle.sector, Some(3));
11452        assert_eq!(angle.inverse, Some(false));
11453        assert_eq!(angle.label_position, Some(label_position));
11454
11455        mock_ctx.close().await;
11456    }
11457
11458    #[tokio::test(flavor = "multi_thread")]
11459    async fn test_edit_angle_constraint_with_call_on_right() {
11460        let initial_source = "\
11461sketch(on = XY) {
11462  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11463  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11464  60deg == angle([line1, line2])
11465}
11466";
11467
11468        let program = Program::parse(initial_source).unwrap().0.unwrap();
11469        let mut frontend = FrontendState::new();
11470        let mock_ctx = ExecutorContext::new_mock(None).await;
11471        let version = Version(0);
11472
11473        frontend.program = program.clone();
11474        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11475        frontend.update_state_after_exec(outcome, true);
11476        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11477        let sketch_id = sketch_object.id;
11478        let sketch = expect_sketch(sketch_object);
11479        let constraint_id = sketch.constraints[0];
11480        let line1_id = *sketch.segments.get(2).unwrap();
11481        let line2_id = *sketch.segments.get(5).unwrap();
11482
11483        let (src_delta, _) = frontend
11484            .edit_angle_constraint_with_options(
11485                &mock_ctx,
11486                version,
11487                sketch_id,
11488                constraint_id,
11489                Angle {
11490                    lines: vec![line2_id, line1_id],
11491                    angle: Number {
11492                        value: 60.0,
11493                        units: NumericSuffix::Deg,
11494                    },
11495                    sector: Some(3),
11496                    inverse: Some(false),
11497                    label_position: None,
11498                    source: Default::default(),
11499                },
11500                EditConstraintOptions {
11501                    commit_solved_initial_guesses: false,
11502                },
11503            )
11504            .await
11505            .unwrap();
11506        assert_eq!(
11507            src_delta.text.as_str(),
11508            "\
11509sketch(on = XY) {
11510  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11511  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11512  60deg == angleDimension(lines = [line2, line1], sector = 3)
11513}
11514"
11515        );
11516
11517        mock_ctx.close().await;
11518    }
11519
11520    #[tokio::test(flavor = "multi_thread")]
11521    async fn test_edit_segments_can_commit_constraint_label_position_in_same_execution() {
11522        let initial_source = "\
11523@settings(kclVersion = 2.0)
11524
11525sketch001 = sketch(on = XZ) {
11526  line1 = line(start = [var 0mm, var 12.55mm], end = [var -6.03mm, var 8.51mm])
11527  line3 = line(start = [var -7.41mm, var 2.92mm], end = [var -1.47mm, var 4.32mm])
11528  distance([line1.start, line3.end], labelPosition = [5.56mm, 8.65mm]) == 8.36mm
11529  vertical([line1.start, ORIGIN])
11530}
11531";
11532
11533        let program = Program::parse(initial_source).unwrap().0.unwrap();
11534        let mut frontend = FrontendState::new();
11535        let mock_ctx = ExecutorContext::new_mock(None).await;
11536        let version = Version(0);
11537
11538        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
11539        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11540        let sketch_id = sketch_object.id;
11541        let sketch = expect_sketch(sketch_object);
11542        let constraint_id = sketch
11543            .constraints
11544            .iter()
11545            .copied()
11546            .find(|constraint_id| {
11547                matches!(
11548                    frontend.scene_graph.objects[constraint_id.0].kind,
11549                    ObjectKind::Constraint {
11550                        constraint: Constraint::Distance(_)
11551                    }
11552                )
11553            })
11554            .unwrap();
11555        let line1_id = sketch
11556            .segments
11557            .iter()
11558            .copied()
11559            .find(|segment_id| {
11560                matches!(
11561                    frontend.scene_graph.objects[segment_id.0].kind,
11562                    ObjectKind::Segment {
11563                        segment: Segment::Line(_)
11564                    }
11565                )
11566            })
11567            .unwrap();
11568        let label_position = Point2d {
11569            x: Number {
11570                value: 7.0,
11571                units: NumericSuffix::Mm,
11572            },
11573            y: Number {
11574                value: 9.0,
11575                units: NumericSuffix::Mm,
11576            },
11577        };
11578
11579        let (source_delta, scene_delta) = frontend
11580            .edit_segments_with_options(
11581                &mock_ctx,
11582                version,
11583                sketch_id,
11584                vec![ExistingSegmentCtor {
11585                    id: line1_id,
11586                    ctor: SegmentCtor::Line(LineCtor {
11587                        start: point_expr_mm(2.0, 15.55),
11588                        end: point_expr_mm(-4.03, 11.51),
11589                        construction: None,
11590                    }),
11591                }],
11592                EditSegmentsOptions {
11593                    anchor_segment_ids: Some(vec![]),
11594                    drag_anchors: vec![SegmentDragAnchor {
11595                        segment_id: line1_id,
11596                        target: label_position.clone(),
11597                    }],
11598                    constraint_label_edits: vec![ConstraintLabelPositionEdit {
11599                        constraint_id,
11600                        label_position: label_position.clone(),
11601                    }],
11602                    commit_solved_initial_guesses: true,
11603                },
11604            )
11605            .await
11606            .unwrap();
11607
11608        assert!(source_delta.text.contains("labelPosition = [7mm, 9mm]"));
11609        let constraint_object = &scene_delta.new_graph.objects[constraint_id.0];
11610        let ObjectKind::Constraint {
11611            constraint: Constraint::Distance(distance),
11612        } = &constraint_object.kind
11613        else {
11614            panic!("Expected distance constraint object");
11615        };
11616        assert_eq!(distance.label_position, Some(label_position));
11617
11618        let snapped_label_position = Point2d {
11619            x: Number {
11620                value: 8.0,
11621                units: NumericSuffix::Mm,
11622            },
11623            y: Number {
11624                value: 10.0,
11625                units: NumericSuffix::Mm,
11626            },
11627        };
11628        let (source_delta, scene_delta) = frontend
11629            .edit_segments_with_options(
11630                &mock_ctx,
11631                version,
11632                sketch_id,
11633                vec![],
11634                EditSegmentsOptions {
11635                    anchor_segment_ids: Some(vec![line1_id]),
11636                    drag_anchors: vec![],
11637                    constraint_label_edits: vec![ConstraintLabelPositionEdit {
11638                        constraint_id,
11639                        label_position: snapped_label_position.clone(),
11640                    }],
11641                    commit_solved_initial_guesses: true,
11642                },
11643            )
11644            .await
11645            .unwrap();
11646
11647        assert!(source_delta.text.contains("labelPosition = [8mm, 10mm]"));
11648        let constraint_object = &scene_delta.new_graph.objects[constraint_id.0];
11649        let ObjectKind::Constraint {
11650            constraint: Constraint::Distance(distance),
11651        } = &constraint_object.kind
11652        else {
11653            panic!("Expected distance constraint object");
11654        };
11655        assert_eq!(distance.label_position, Some(snapped_label_position));
11656
11657        mock_ctx.close().await;
11658    }
11659
11660    #[tokio::test(flavor = "multi_thread")]
11661    async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
11662        let initial_source = "\
11663sketch(on = XY) {
11664  point1 = point(at = [var 0mm, var 0mm])
11665  point2 = point(at = [var 10mm, var 0mm])
11666  distance([point1, point2]) == 5mm
11667}
11668";
11669
11670        let program = Program::parse(initial_source).unwrap().0.unwrap();
11671        let mut frontend = FrontendState::new();
11672        let mock_ctx = ExecutorContext::new_mock(None).await;
11673        let version = Version(0);
11674
11675        frontend.program = program.clone();
11676        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11677        frontend.update_state_after_exec(outcome, true);
11678        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11679        let sketch_id = sketch_object.id;
11680        let sketch = expect_sketch(sketch_object);
11681        let point0_id = sketch.segments[0];
11682        let point1_id = sketch.segments[1];
11683        let constraint_id = sketch.constraints[0];
11684
11685        let edited_segments = vec![ExistingSegmentCtor {
11686            id: point0_id,
11687            ctor: SegmentCtor::Point(PointCtor {
11688                position: Point2d {
11689                    x: Expr::Var(Number {
11690                        value: 2.0,
11691                        units: NumericSuffix::Mm,
11692                    }),
11693                    y: Expr::Var(Number {
11694                        value: 1.0,
11695                        units: NumericSuffix::Mm,
11696                    }),
11697                },
11698            }),
11699        }];
11700        let (_, scene_delta) = frontend
11701            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
11702            .await
11703            .unwrap();
11704        let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
11705        let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
11706
11707        let label_position = Point2d {
11708            x: Number {
11709                value: 3.0,
11710                units: NumericSuffix::Mm,
11711            },
11712            y: Number {
11713                value: 4.0,
11714                units: NumericSuffix::Mm,
11715            },
11716        };
11717        let (_, scene_delta) = frontend
11718            .edit_distance_constraint_label_position(
11719                &mock_ctx,
11720                version,
11721                sketch_id,
11722                constraint_id,
11723                label_position,
11724                vec![point0_id],
11725            )
11726            .await
11727            .unwrap();
11728
11729        assert_point_position_close(
11730            point_position(&scene_delta.new_graph, point0_id),
11731            point0_after_segment_edit,
11732        );
11733        assert_point_position_close(
11734            point_position(&scene_delta.new_graph, point1_id),
11735            point1_after_segment_edit,
11736        );
11737
11738        mock_ctx.close().await;
11739    }
11740
11741    #[tokio::test(flavor = "multi_thread")]
11742    async fn test_distance_point_line() {
11743        let initial_source = "\
11744sketch(on = XY) {
11745  point(at = [var 0, var 5])
11746  line(start = [var 0, var 0], end = [var 10, var 0])
11747}
11748";
11749
11750        let program = Program::parse(initial_source).unwrap().0.unwrap();
11751
11752        let mut frontend = FrontendState::new();
11753
11754        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11755        let mock_ctx = ExecutorContext::new_mock(None).await;
11756        let version = Version(0);
11757
11758        frontend.hack_set_program(&ctx, program).await.unwrap();
11759        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11760        let sketch_id = sketch_object.id;
11761        let sketch = expect_sketch(sketch_object);
11762        let point_id = *sketch.segments.first().unwrap();
11763        let line_id = *sketch
11764            .segments
11765            .iter()
11766            .find(|segment_id| {
11767                matches!(
11768                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11769                    Some(ObjectKind::Segment {
11770                        segment: Segment::Line(_)
11771                    })
11772                )
11773            })
11774            .unwrap();
11775
11776        let label_position = Point2d {
11777            x: Number {
11778                value: 10.0,
11779                units: NumericSuffix::Mm,
11780            },
11781            y: Number {
11782                value: 11.0,
11783                units: NumericSuffix::Mm,
11784            },
11785        };
11786        let constraint = Constraint::Distance(Distance {
11787            segments: vec![point_id.into(), line_id.into()],
11788            distance: Number {
11789                value: 5.0,
11790                units: NumericSuffix::Mm,
11791            },
11792            label_position: Some(label_position.clone()),
11793            source: Default::default(),
11794        });
11795        let (src_delta, scene_delta) = frontend
11796            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11797            .await
11798            .unwrap();
11799        insta::assert_snapshot!("test_distance_point_line", src_delta.text.as_str());
11800        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11801        let sketch = expect_sketch(sketch_object);
11802        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11803        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11804            panic!("Expected constraint object");
11805        };
11806        let Constraint::Distance(distance) = constraint else {
11807            panic!("Expected distance constraint");
11808        };
11809        assert_eq!(distance.label_position, Some(label_position));
11810
11811        ctx.close().await;
11812        mock_ctx.close().await;
11813    }
11814
11815    #[tokio::test(flavor = "multi_thread")]
11816    async fn test_distance_point_arc() {
11817        let initial_source = "\
11818sketch(on = XY) {
11819  point(at = [var 0, var 8])
11820  arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
11821}
11822";
11823
11824        let program = Program::parse(initial_source).unwrap().0.unwrap();
11825
11826        let mut frontend = FrontendState::new();
11827
11828        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11829        let mock_ctx = ExecutorContext::new_mock(None).await;
11830        let version = Version(0);
11831
11832        frontend.hack_set_program(&ctx, program).await.unwrap();
11833        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11834        let sketch_id = sketch_object.id;
11835        let sketch = expect_sketch(sketch_object);
11836        let point_id = *sketch.segments.first().unwrap();
11837        let arc_id = *sketch
11838            .segments
11839            .iter()
11840            .find(|segment_id| {
11841                matches!(
11842                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11843                    Some(ObjectKind::Segment {
11844                        segment: Segment::Arc(_)
11845                    })
11846                )
11847            })
11848            .unwrap();
11849
11850        let constraint = Constraint::Distance(Distance {
11851            segments: vec![point_id.into(), arc_id.into()],
11852            distance: Number {
11853                value: 3.0,
11854                units: NumericSuffix::Mm,
11855            },
11856            label_position: None,
11857            source: Default::default(),
11858        });
11859        let (src_delta, _scene_delta) = frontend
11860            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11861            .await
11862            .unwrap();
11863        insta::assert_snapshot!("test_distance_point_arc", src_delta.text.as_str());
11864
11865        ctx.close().await;
11866        mock_ctx.close().await;
11867    }
11868
11869    #[tokio::test(flavor = "multi_thread")]
11870    async fn test_distance_arc_origin() {
11871        let initial_source = "\
11872sketch001 = sketch(on = XY) {
11873  arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
11874}
11875";
11876
11877        let program = Program::parse(initial_source).unwrap().0.unwrap();
11878
11879        let mut frontend = FrontendState::new();
11880
11881        let mock_ctx = ExecutorContext::new_mock(None).await;
11882        let version = Version(0);
11883
11884        frontend.program = program.clone();
11885        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11886        frontend.update_state_after_exec(outcome, true);
11887        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11888        let sketch_id = sketch_object.id;
11889        let sketch = expect_sketch(sketch_object);
11890        let arc_id = *sketch
11891            .segments
11892            .iter()
11893            .find(|segment_id| {
11894                matches!(
11895                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11896                    Some(ObjectKind::Segment {
11897                        segment: Segment::Arc(_)
11898                    })
11899                )
11900            })
11901            .unwrap();
11902
11903        let constraint = Constraint::Distance(Distance {
11904            segments: vec![arc_id.into(), ConstraintSegment::ORIGIN],
11905            distance: Number {
11906                value: 3.0,
11907                units: NumericSuffix::Mm,
11908            },
11909            label_position: None,
11910            source: Default::default(),
11911        });
11912        let (src_delta, _scene_delta) = frontend
11913            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11914            .await
11915            .unwrap();
11916        insta::assert_snapshot!("test_distance_arc_origin", src_delta.text.as_str());
11917
11918        mock_ctx.close().await;
11919    }
11920
11921    #[tokio::test(flavor = "multi_thread")]
11922    async fn test_distance_line_origin() {
11923        let initial_source = "\
11924sketch(on = XY) {
11925  line(start = [var 5, var 0], end = [var 5, var 10])
11926}
11927";
11928
11929        let program = Program::parse(initial_source).unwrap().0.unwrap();
11930
11931        let mut frontend = FrontendState::new();
11932
11933        let mock_ctx = ExecutorContext::new_mock(None).await;
11934        let version = Version(0);
11935
11936        frontend.program = program.clone();
11937        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11938        frontend.update_state_after_exec(outcome, true);
11939        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11940        let sketch_id = sketch_object.id;
11941        let sketch = expect_sketch(sketch_object);
11942        let line_id = *sketch
11943            .segments
11944            .iter()
11945            .find(|segment_id| {
11946                matches!(
11947                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11948                    Some(ObjectKind::Segment {
11949                        segment: Segment::Line(_)
11950                    })
11951                )
11952            })
11953            .unwrap();
11954
11955        let constraint = Constraint::Distance(Distance {
11956            segments: vec![ConstraintSegment::ORIGIN, line_id.into()],
11957            distance: Number {
11958                value: 5.0,
11959                units: NumericSuffix::Mm,
11960            },
11961            label_position: None,
11962            source: Default::default(),
11963        });
11964        let (src_delta, _scene_delta) = frontend
11965            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11966            .await
11967            .unwrap();
11968        insta::assert_snapshot!("test_distance_line_origin", src_delta.text.as_str());
11969
11970        mock_ctx.close().await;
11971    }
11972
11973    #[tokio::test(flavor = "multi_thread")]
11974    async fn test_distance_line_circle() {
11975        let initial_source = "\
11976sketch(on = XY) {
11977  line(start = [var -10, var 8], end = [var 10, var 8])
11978  circle(start = [var 5, var 0], center = [var 0, var 0])
11979}
11980";
11981
11982        let program = Program::parse(initial_source).unwrap().0.unwrap();
11983
11984        let mut frontend = FrontendState::new();
11985
11986        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11987        let mock_ctx = ExecutorContext::new_mock(None).await;
11988        let version = Version(0);
11989
11990        frontend.hack_set_program(&ctx, program).await.unwrap();
11991        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11992        let sketch_id = sketch_object.id;
11993        let sketch = expect_sketch(sketch_object);
11994        let line_id = *sketch
11995            .segments
11996            .iter()
11997            .find(|segment_id| {
11998                matches!(
11999                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12000                    Some(ObjectKind::Segment {
12001                        segment: Segment::Line(_)
12002                    })
12003                )
12004            })
12005            .unwrap();
12006        let circle_id = *sketch
12007            .segments
12008            .iter()
12009            .find(|segment_id| {
12010                matches!(
12011                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12012                    Some(ObjectKind::Segment {
12013                        segment: Segment::Circle(_)
12014                    })
12015                )
12016            })
12017            .unwrap();
12018
12019        let constraint = Constraint::Distance(Distance {
12020            segments: vec![line_id.into(), circle_id.into()],
12021            distance: Number {
12022                value: 3.0,
12023                units: NumericSuffix::Mm,
12024            },
12025            label_position: None,
12026            source: Default::default(),
12027        });
12028        let (src_delta, _scene_delta) = frontend
12029            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12030            .await
12031            .unwrap();
12032        insta::assert_snapshot!("test_distance_line_circle", src_delta.text.as_str());
12033
12034        ctx.close().await;
12035        mock_ctx.close().await;
12036    }
12037
12038    #[tokio::test(flavor = "multi_thread")]
12039    async fn test_distance_circle_arc() {
12040        let initial_source = "\
12041sketch(on = XY) {
12042  circle(start = [var 5, var 0], center = [var 0, var 0])
12043  arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
12044}
12045";
12046
12047        let program = Program::parse(initial_source).unwrap().0.unwrap();
12048
12049        let mut frontend = FrontendState::new();
12050
12051        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12052        let mock_ctx = ExecutorContext::new_mock(None).await;
12053        let version = Version(0);
12054
12055        frontend.hack_set_program(&ctx, program).await.unwrap();
12056        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12057        let sketch_id = sketch_object.id;
12058        let sketch = expect_sketch(sketch_object);
12059        let circle_id = *sketch
12060            .segments
12061            .iter()
12062            .find(|segment_id| {
12063                matches!(
12064                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12065                    Some(ObjectKind::Segment {
12066                        segment: Segment::Circle(_)
12067                    })
12068                )
12069            })
12070            .unwrap();
12071        let arc_id = *sketch
12072            .segments
12073            .iter()
12074            .find(|segment_id| {
12075                matches!(
12076                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12077                    Some(ObjectKind::Segment {
12078                        segment: Segment::Arc(_)
12079                    })
12080                )
12081            })
12082            .unwrap();
12083
12084        let constraint = Constraint::Distance(Distance {
12085            segments: vec![circle_id.into(), arc_id.into()],
12086            distance: Number {
12087                value: 3.0,
12088                units: NumericSuffix::Mm,
12089            },
12090            label_position: None,
12091            source: Default::default(),
12092        });
12093        let (src_delta, _scene_delta) = frontend
12094            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12095            .await
12096            .unwrap();
12097        insta::assert_snapshot!("test_distance_circle_arc", src_delta.text.as_str());
12098
12099        ctx.close().await;
12100        mock_ctx.close().await;
12101    }
12102
12103    #[tokio::test(flavor = "multi_thread")]
12104    async fn test_distance_parallel_lines() {
12105        let initial_source = "\
12106sketch(on = XY) {
12107  line(start = [var 0, var 0], end = [var 10, var 0])
12108  line(start = [var 0, var 5], end = [var 10, var 5])
12109}
12110";
12111
12112        let program = Program::parse(initial_source).unwrap().0.unwrap();
12113
12114        let mut frontend = FrontendState::new();
12115
12116        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12117        let mock_ctx = ExecutorContext::new_mock(None).await;
12118        let version = Version(0);
12119
12120        frontend.hack_set_program(&ctx, program).await.unwrap();
12121        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12122        let sketch_id = sketch_object.id;
12123        let sketch = expect_sketch(sketch_object);
12124        let line_ids = sketch
12125            .segments
12126            .iter()
12127            .copied()
12128            .filter(|segment_id| {
12129                matches!(
12130                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12131                    Some(ObjectKind::Segment {
12132                        segment: Segment::Line(_)
12133                    })
12134                )
12135            })
12136            .collect::<Vec<_>>();
12137
12138        let constraint = Constraint::Distance(Distance {
12139            segments: vec![line_ids[0].into(), line_ids[1].into()],
12140            distance: Number {
12141                value: 5.0,
12142                units: NumericSuffix::Mm,
12143            },
12144            label_position: None,
12145            source: Default::default(),
12146        });
12147        let (src_delta, _scene_delta) = frontend
12148            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12149            .await
12150            .unwrap();
12151        insta::assert_snapshot!("test_distance_parallel_lines", src_delta.text.as_str());
12152
12153        ctx.close().await;
12154        mock_ctx.close().await;
12155    }
12156
12157    #[tokio::test(flavor = "multi_thread")]
12158    async fn test_distance_non_parallel_lines_lowers_to_distance() {
12159        // NOTE: Current LinesAtAngle constraint collapses if lines are initialized perpendicular to
12160        // one another because the gradient of the residual has no tangential component in this
12161        // configuration. The only path to reducing the residual is shrinking the lengths of the
12162        // lines which are causing the lines to collapse, producing a degenerate output.
12163        let initial_source = "\
12164sketch(on = XY) {
12165  line(start = [var 0, var 0], end = [var 10, var 0])
12166  line(start = [var 0, var 0], end = [var 10, var 10])
12167}
12168";
12169
12170        let program = Program::parse(initial_source).unwrap().0.unwrap();
12171
12172        let mut frontend = FrontendState::new();
12173
12174        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12175        let mock_ctx = ExecutorContext::new_mock(None).await;
12176        let version = Version(0);
12177
12178        frontend.hack_set_program(&ctx, program).await.unwrap();
12179        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12180        let sketch_id = sketch_object.id;
12181        let sketch = expect_sketch(sketch_object);
12182        let line_ids = sketch
12183            .segments
12184            .iter()
12185            .copied()
12186            .filter(|segment_id| {
12187                matches!(
12188                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12189                    Some(ObjectKind::Segment {
12190                        segment: Segment::Line(_)
12191                    })
12192                )
12193            })
12194            .collect::<Vec<_>>();
12195
12196        let constraint = Constraint::Distance(Distance {
12197            segments: vec![line_ids[0].into(), line_ids[1].into()],
12198            distance: Number {
12199                value: 5.0,
12200                units: NumericSuffix::Mm,
12201            },
12202            label_position: None,
12203            source: Default::default(),
12204        });
12205        let (src_delta, _scene_delta) = frontend
12206            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12207            .await
12208            .unwrap();
12209        insta::assert_snapshot!(
12210            "test_distance_non_parallel_lines_lowers_to_distance",
12211            src_delta.text.as_str()
12212        );
12213
12214        ctx.close().await;
12215        mock_ctx.close().await;
12216    }
12217
12218    #[tokio::test(flavor = "multi_thread")]
12219    async fn test_horizontal_distance_two_points() {
12220        let initial_source = "\
12221sketch(on = XY) {
12222  point(at = [var 1, var 2])
12223  point(at = [var 3, var 4])
12224}
12225";
12226
12227        let program = Program::parse(initial_source).unwrap().0.unwrap();
12228
12229        let mut frontend = FrontendState::new();
12230
12231        let mock_ctx = ExecutorContext::new_mock(None).await;
12232        let version = Version(0);
12233
12234        frontend.program = program.clone();
12235        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12236        frontend.update_state_after_exec(outcome, true);
12237        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12238        let sketch_id = sketch_object.id;
12239        let sketch = expect_sketch(sketch_object);
12240        let point0_id = *sketch.segments.first().unwrap();
12241        let point1_id = *sketch.segments.get(1).unwrap();
12242        let label_position = Point2d {
12243            x: Number {
12244                value: 10.0,
12245                units: NumericSuffix::Mm,
12246            },
12247            y: Number {
12248                value: 11.0,
12249                units: NumericSuffix::Mm,
12250            },
12251        };
12252
12253        let constraint = Constraint::HorizontalDistance(Distance {
12254            segments: vec![point0_id.into(), point1_id.into()],
12255            distance: Number {
12256                value: 2.0,
12257                units: NumericSuffix::Mm,
12258            },
12259            label_position: Some(label_position.clone()),
12260            source: Default::default(),
12261        });
12262        let (src_delta, scene_delta) = frontend
12263            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12264            .await
12265            .unwrap();
12266        insta::assert_snapshot!("test_horizontal_distance_two_points", src_delta.text.as_str());
12267        assert_eq!(
12268            scene_delta.new_graph.objects.len(),
12269            5,
12270            "{:#?}",
12271            scene_delta.new_graph.objects
12272        );
12273        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12274        let sketch = expect_sketch(sketch_object);
12275        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12276        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12277            panic!("Expected constraint object");
12278        };
12279        let Constraint::HorizontalDistance(distance) = constraint else {
12280            panic!("Expected horizontal distance constraint");
12281        };
12282        assert_eq!(distance.label_position, Some(label_position));
12283
12284        mock_ctx.close().await;
12285    }
12286
12287    #[tokio::test(flavor = "multi_thread")]
12288    async fn test_radius_single_arc_segment() {
12289        let initial_source = "\
12290sketch(on = XY) {
12291  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12292}
12293";
12294
12295        let program = Program::parse(initial_source).unwrap().0.unwrap();
12296
12297        let mut frontend = FrontendState::new();
12298
12299        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12300        let mock_ctx = ExecutorContext::new_mock(None).await;
12301        let version = Version(0);
12302
12303        frontend.hack_set_program(&ctx, program).await.unwrap();
12304        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12305        let sketch_id = sketch_object.id;
12306        let sketch = expect_sketch(sketch_object);
12307        // Find the arc segment (not the points)
12308        let arc_id = sketch
12309            .segments
12310            .iter()
12311            .find(|&seg_id| {
12312                let obj = frontend.scene_graph.objects.get(seg_id.0);
12313                matches!(
12314                    obj.map(|o| &o.kind),
12315                    Some(ObjectKind::Segment {
12316                        segment: Segment::Arc(_)
12317                    })
12318                )
12319            })
12320            .unwrap();
12321
12322        let constraint = Constraint::Radius(Radius {
12323            arc: *arc_id,
12324            radius: Number {
12325                value: 5.0,
12326                units: NumericSuffix::Mm,
12327            },
12328            label_position: None,
12329            source: Default::default(),
12330        });
12331        let (src_delta, scene_delta) = frontend
12332            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12333            .await
12334            .unwrap();
12335        insta::assert_snapshot!("test_radius_single_arc_segment", src_delta.text.as_str());
12336        assert_eq!(
12337            scene_delta.new_graph.objects.len(),
12338            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
12339            "{:#?}",
12340            scene_delta.new_graph.objects
12341        );
12342
12343        ctx.close().await;
12344        mock_ctx.close().await;
12345    }
12346
12347    #[tokio::test(flavor = "multi_thread")]
12348    async fn test_radius_single_arc_segment_with_label_position() {
12349        let initial_source = "\
12350sketch(on = XY) {
12351  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12352}
12353";
12354
12355        let program = Program::parse(initial_source).unwrap().0.unwrap();
12356        let mut frontend = FrontendState::new();
12357        let mock_ctx = ExecutorContext::new_mock(None).await;
12358        let version = Version(0);
12359
12360        frontend.program = program.clone();
12361        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12362        frontend.update_state_after_exec(outcome, true);
12363        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12364        let sketch_id = sketch_object.id;
12365        let sketch = expect_sketch(sketch_object);
12366        let arc_id = sketch
12367            .segments
12368            .iter()
12369            .find(|&seg_id| {
12370                let obj = frontend.scene_graph.objects.get(seg_id.0);
12371                matches!(
12372                    obj.map(|o| &o.kind),
12373                    Some(ObjectKind::Segment {
12374                        segment: Segment::Arc(_)
12375                    })
12376                )
12377            })
12378            .unwrap();
12379
12380        let label_position = Point2d {
12381            x: Number {
12382                value: 10.0,
12383                units: NumericSuffix::Mm,
12384            },
12385            y: Number {
12386                value: 11.0,
12387                units: NumericSuffix::Mm,
12388            },
12389        };
12390        let constraint = Constraint::Radius(Radius {
12391            arc: *arc_id,
12392            radius: Number {
12393                value: 5.0,
12394                units: NumericSuffix::Mm,
12395            },
12396            label_position: Some(label_position.clone()),
12397            source: Default::default(),
12398        });
12399        let (src_delta, scene_delta) = frontend
12400            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12401            .await
12402            .unwrap();
12403        insta::assert_snapshot!(
12404            "test_radius_single_arc_segment_with_label_position",
12405            src_delta.text.as_str()
12406        );
12407
12408        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12409        let sketch = expect_sketch(sketch_object);
12410        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12411        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12412            panic!("Expected constraint object");
12413        };
12414        let Constraint::Radius(radius) = constraint else {
12415            panic!("Expected radius constraint");
12416        };
12417        assert_eq!(radius.label_position, Some(label_position));
12418
12419        mock_ctx.close().await;
12420    }
12421
12422    #[tokio::test(flavor = "multi_thread")]
12423    async fn test_edit_radius_constraint_label_position() {
12424        let initial_source = "\
12425sketch(on = XY) {
12426  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12427  radius(arc1) == 5mm
12428}
12429";
12430
12431        let program = Program::parse(initial_source).unwrap().0.unwrap();
12432        let mut frontend = FrontendState::new();
12433        let mock_ctx = ExecutorContext::new_mock(None).await;
12434        let version = Version(0);
12435
12436        frontend.program = program.clone();
12437        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12438        frontend.update_state_after_exec(outcome, true);
12439        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12440        let sketch_id = sketch_object.id;
12441        let sketch = expect_sketch(sketch_object);
12442        let constraint_id = sketch.constraints[0];
12443        let label_position = Point2d {
12444            x: Number {
12445                value: 10.0,
12446                units: NumericSuffix::Mm,
12447            },
12448            y: Number {
12449                value: 11.0,
12450                units: NumericSuffix::Mm,
12451            },
12452        };
12453
12454        let (src_delta, scene_delta) = frontend
12455            .edit_distance_constraint_label_position(
12456                &mock_ctx,
12457                version,
12458                sketch_id,
12459                constraint_id,
12460                label_position.clone(),
12461                vec![],
12462            )
12463            .await
12464            .unwrap();
12465        insta::assert_snapshot!("test_edit_radius_constraint_label_position", src_delta.text.as_str());
12466
12467        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12468        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12469            panic!("Expected constraint object");
12470        };
12471        let Constraint::Radius(radius) = constraint else {
12472            panic!("Expected radius constraint");
12473        };
12474        assert_eq!(radius.label_position, Some(label_position));
12475
12476        mock_ctx.close().await;
12477    }
12478
12479    #[tokio::test(flavor = "multi_thread")]
12480    async fn test_vertical_distance_two_points() {
12481        let initial_source = "\
12482sketch(on = XY) {
12483  point(at = [var 1, var 2])
12484  point(at = [var 3, var 4])
12485}
12486";
12487
12488        let program = Program::parse(initial_source).unwrap().0.unwrap();
12489
12490        let mut frontend = FrontendState::new();
12491
12492        let mock_ctx = ExecutorContext::new_mock(None).await;
12493        let version = Version(0);
12494
12495        frontend.program = program.clone();
12496        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12497        frontend.update_state_after_exec(outcome, true);
12498        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12499        let sketch_id = sketch_object.id;
12500        let sketch = expect_sketch(sketch_object);
12501        let point0_id = *sketch.segments.first().unwrap();
12502        let point1_id = *sketch.segments.get(1).unwrap();
12503        let label_position = Point2d {
12504            x: Number {
12505                value: 10.0,
12506                units: NumericSuffix::Mm,
12507            },
12508            y: Number {
12509                value: 11.0,
12510                units: NumericSuffix::Mm,
12511            },
12512        };
12513
12514        let constraint = Constraint::VerticalDistance(Distance {
12515            segments: vec![point0_id.into(), point1_id.into()],
12516            distance: Number {
12517                value: 2.0,
12518                units: NumericSuffix::Mm,
12519            },
12520            label_position: Some(label_position.clone()),
12521            source: Default::default(),
12522        });
12523        let (src_delta, scene_delta) = frontend
12524            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12525            .await
12526            .unwrap();
12527        insta::assert_snapshot!("test_vertical_distance_two_points", src_delta.text.as_str());
12528        assert_eq!(
12529            scene_delta.new_graph.objects.len(),
12530            5,
12531            "{:#?}",
12532            scene_delta.new_graph.objects
12533        );
12534        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12535        let sketch = expect_sketch(sketch_object);
12536        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12537        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12538            panic!("Expected constraint object");
12539        };
12540        let Constraint::VerticalDistance(distance) = constraint else {
12541            panic!("Expected vertical distance constraint");
12542        };
12543        assert_eq!(distance.label_position, Some(label_position));
12544
12545        mock_ctx.close().await;
12546    }
12547
12548    #[tokio::test(flavor = "multi_thread")]
12549    async fn test_add_fixed_standalone_point() {
12550        let initial_source = "\
12551sketch(on = XY) {
12552  point(at = [var 1, var 2])
12553}
12554";
12555
12556        let program = Program::parse(initial_source).unwrap().0.unwrap();
12557
12558        let mut frontend = FrontendState::new();
12559
12560        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12561        let mock_ctx = ExecutorContext::new_mock(None).await;
12562        let version = Version(0);
12563
12564        frontend.hack_set_program(&ctx, program).await.unwrap();
12565        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12566        let sketch_id = sketch_object.id;
12567        let sketch = expect_sketch(sketch_object);
12568        let point_id = *sketch.segments.first().unwrap();
12569
12570        let (src_delta, scene_delta) = frontend
12571            .add_constraint(
12572                &mock_ctx,
12573                version,
12574                sketch_id,
12575                Constraint::Fixed(Fixed {
12576                    points: vec![FixedPoint {
12577                        point: point_id,
12578                        position: Point2d {
12579                            x: Number {
12580                                value: 2.0,
12581                                units: NumericSuffix::Mm,
12582                            },
12583                            y: Number {
12584                                value: 3.0,
12585                                units: NumericSuffix::Mm,
12586                            },
12587                        },
12588                    }],
12589                }),
12590            )
12591            .await
12592            .unwrap();
12593        insta::assert_snapshot!("test_add_fixed_standalone_point", src_delta.text.as_str());
12594        assert_eq!(
12595            scene_delta.new_graph.objects.len(),
12596            4,
12597            "{:#?}",
12598            scene_delta.new_graph.objects
12599        );
12600
12601        ctx.close().await;
12602        mock_ctx.close().await;
12603    }
12604
12605    #[tokio::test(flavor = "multi_thread")]
12606    async fn test_add_fixed_multiple_points() {
12607        let initial_source = "\
12608sketch(on = XY) {
12609  point(at = [var 1, var 2])
12610  point(at = [var 3, var 4])
12611}
12612";
12613
12614        let program = Program::parse(initial_source).unwrap().0.unwrap();
12615
12616        let mut frontend = FrontendState::new();
12617
12618        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12619        let mock_ctx = ExecutorContext::new_mock(None).await;
12620        let version = Version(0);
12621
12622        frontend.hack_set_program(&ctx, program).await.unwrap();
12623        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12624        let sketch_id = sketch_object.id;
12625        let sketch = expect_sketch(sketch_object);
12626        let point0_id = *sketch.segments.first().unwrap();
12627        let point1_id = *sketch.segments.get(1).unwrap();
12628
12629        let (src_delta, scene_delta) = frontend
12630            .add_constraint(
12631                &mock_ctx,
12632                version,
12633                sketch_id,
12634                Constraint::Fixed(Fixed {
12635                    points: vec![
12636                        FixedPoint {
12637                            point: point0_id,
12638                            position: Point2d {
12639                                x: Number {
12640                                    value: 2.0,
12641                                    units: NumericSuffix::Mm,
12642                                },
12643                                y: Number {
12644                                    value: 3.0,
12645                                    units: NumericSuffix::Mm,
12646                                },
12647                            },
12648                        },
12649                        FixedPoint {
12650                            point: point1_id,
12651                            position: Point2d {
12652                                x: Number {
12653                                    value: 4.0,
12654                                    units: NumericSuffix::Mm,
12655                                },
12656                                y: Number {
12657                                    value: 5.0,
12658                                    units: NumericSuffix::Mm,
12659                                },
12660                            },
12661                        },
12662                    ],
12663                }),
12664            )
12665            .await
12666            .unwrap();
12667        insta::assert_snapshot!("test_add_fixed_multiple_points", src_delta.text.as_str());
12668        assert_eq!(
12669            scene_delta.new_graph.objects.len(),
12670            6,
12671            "{:#?}",
12672            scene_delta.new_graph.objects
12673        );
12674
12675        ctx.close().await;
12676        mock_ctx.close().await;
12677    }
12678
12679    #[tokio::test(flavor = "multi_thread")]
12680    async fn test_add_fixed_owned_point() {
12681        let initial_source = "\
12682sketch(on = XY) {
12683  line(start = [var 1, var 2], end = [var 3, var 4])
12684}
12685";
12686
12687        let program = Program::parse(initial_source).unwrap().0.unwrap();
12688
12689        let mut frontend = FrontendState::new();
12690
12691        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12692        let mock_ctx = ExecutorContext::new_mock(None).await;
12693        let version = Version(0);
12694
12695        frontend.hack_set_program(&ctx, program).await.unwrap();
12696        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12697        let sketch_id = sketch_object.id;
12698        let sketch = expect_sketch(sketch_object);
12699        let line_start_id = *sketch.segments.first().unwrap();
12700
12701        let (src_delta, scene_delta) = frontend
12702            .add_constraint(
12703                &mock_ctx,
12704                version,
12705                sketch_id,
12706                Constraint::Fixed(Fixed {
12707                    points: vec![FixedPoint {
12708                        point: line_start_id,
12709                        position: Point2d {
12710                            x: Number {
12711                                value: 2.0,
12712                                units: NumericSuffix::Mm,
12713                            },
12714                            y: Number {
12715                                value: 3.0,
12716                                units: NumericSuffix::Mm,
12717                            },
12718                        },
12719                    }],
12720                }),
12721            )
12722            .await
12723            .unwrap();
12724        insta::assert_snapshot!("test_add_fixed_owned_point", src_delta.text.as_str());
12725        assert_eq!(
12726            scene_delta.new_graph.objects.len(),
12727            6,
12728            "{:#?}",
12729            scene_delta.new_graph.objects
12730        );
12731
12732        ctx.close().await;
12733        mock_ctx.close().await;
12734    }
12735
12736    #[tokio::test(flavor = "multi_thread")]
12737    async fn test_radius_error_cases() {
12738        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12739        let mock_ctx = ExecutorContext::new_mock(None).await;
12740        let version = Version(0);
12741
12742        // Test: Single point should error
12743        let initial_source_point = "\
12744sketch(on = XY) {
12745  point(at = [var 1, var 2])
12746}
12747";
12748        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12749        let mut frontend_point = FrontendState::new();
12750        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12751        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12752        let sketch_id_point = sketch_object_point.id;
12753        let sketch_point = expect_sketch(sketch_object_point);
12754        let point_id = *sketch_point.segments.first().unwrap();
12755
12756        let constraint_point = Constraint::Radius(Radius {
12757            arc: point_id,
12758            radius: Number {
12759                value: 5.0,
12760                units: NumericSuffix::Mm,
12761            },
12762            label_position: None,
12763            source: Default::default(),
12764        });
12765        let result_point = frontend_point
12766            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12767            .await;
12768        assert!(result_point.is_err(), "Single point should error for radius");
12769
12770        // Test: Single line segment should error (only arc segments supported)
12771        let initial_source_line = "\
12772sketch(on = XY) {
12773  line(start = [var 1, var 2], end = [var 3, var 4])
12774}
12775";
12776        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12777        let mut frontend_line = FrontendState::new();
12778        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12779        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12780        let sketch_id_line = sketch_object_line.id;
12781        let sketch_line = expect_sketch(sketch_object_line);
12782        let line_id = *sketch_line.segments.first().unwrap();
12783
12784        let constraint_line = Constraint::Radius(Radius {
12785            arc: line_id,
12786            radius: Number {
12787                value: 5.0,
12788                units: NumericSuffix::Mm,
12789            },
12790            label_position: None,
12791            source: Default::default(),
12792        });
12793        let result_line = frontend_line
12794            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12795            .await;
12796        assert!(result_line.is_err(), "Single line segment should error for radius");
12797
12798        ctx.close().await;
12799        mock_ctx.close().await;
12800    }
12801
12802    #[tokio::test(flavor = "multi_thread")]
12803    async fn test_diameter_single_arc_segment() {
12804        let initial_source = "\
12805sketch(on = XY) {
12806  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12807}
12808";
12809
12810        let program = Program::parse(initial_source).unwrap().0.unwrap();
12811
12812        let mut frontend = FrontendState::new();
12813
12814        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12815        let mock_ctx = ExecutorContext::new_mock(None).await;
12816        let version = Version(0);
12817
12818        frontend.hack_set_program(&ctx, program).await.unwrap();
12819        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12820        let sketch_id = sketch_object.id;
12821        let sketch = expect_sketch(sketch_object);
12822        // Find the arc segment (not the points)
12823        let arc_id = sketch
12824            .segments
12825            .iter()
12826            .find(|&seg_id| {
12827                let obj = frontend.scene_graph.objects.get(seg_id.0);
12828                matches!(
12829                    obj.map(|o| &o.kind),
12830                    Some(ObjectKind::Segment {
12831                        segment: Segment::Arc(_)
12832                    })
12833                )
12834            })
12835            .unwrap();
12836
12837        let constraint = Constraint::Diameter(Diameter {
12838            arc: *arc_id,
12839            diameter: Number {
12840                value: 10.0,
12841                units: NumericSuffix::Mm,
12842            },
12843            label_position: None,
12844            source: Default::default(),
12845        });
12846        let (src_delta, scene_delta) = frontend
12847            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12848            .await
12849            .unwrap();
12850        insta::assert_snapshot!("test_diameter_single_arc_segment", src_delta.text.as_str());
12851        assert_eq!(
12852            scene_delta.new_graph.objects.len(),
12853            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
12854            "{:#?}",
12855            scene_delta.new_graph.objects
12856        );
12857
12858        ctx.close().await;
12859        mock_ctx.close().await;
12860    }
12861
12862    #[tokio::test(flavor = "multi_thread")]
12863    async fn test_diameter_single_arc_segment_with_label_position() {
12864        let initial_source = "\
12865sketch(on = XY) {
12866  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12867}
12868";
12869
12870        let program = Program::parse(initial_source).unwrap().0.unwrap();
12871        let mut frontend = FrontendState::new();
12872        let mock_ctx = ExecutorContext::new_mock(None).await;
12873        let version = Version(0);
12874
12875        frontend.program = program.clone();
12876        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12877        frontend.update_state_after_exec(outcome, true);
12878        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12879        let sketch_id = sketch_object.id;
12880        let sketch = expect_sketch(sketch_object);
12881        let arc_id = sketch
12882            .segments
12883            .iter()
12884            .find(|&seg_id| {
12885                let obj = frontend.scene_graph.objects.get(seg_id.0);
12886                matches!(
12887                    obj.map(|o| &o.kind),
12888                    Some(ObjectKind::Segment {
12889                        segment: Segment::Arc(_)
12890                    })
12891                )
12892            })
12893            .unwrap();
12894
12895        let label_position = Point2d {
12896            x: Number {
12897                value: 10.0,
12898                units: NumericSuffix::Mm,
12899            },
12900            y: Number {
12901                value: 11.0,
12902                units: NumericSuffix::Mm,
12903            },
12904        };
12905        let constraint = Constraint::Diameter(Diameter {
12906            arc: *arc_id,
12907            diameter: Number {
12908                value: 10.0,
12909                units: NumericSuffix::Mm,
12910            },
12911            label_position: Some(label_position.clone()),
12912            source: Default::default(),
12913        });
12914        let (src_delta, scene_delta) = frontend
12915            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12916            .await
12917            .unwrap();
12918        insta::assert_snapshot!(
12919            "test_diameter_single_arc_segment_with_label_position",
12920            src_delta.text.as_str()
12921        );
12922
12923        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12924        let sketch = expect_sketch(sketch_object);
12925        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12926        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12927            panic!("Expected constraint object");
12928        };
12929        let Constraint::Diameter(diameter) = constraint else {
12930            panic!("Expected diameter constraint");
12931        };
12932        assert_eq!(diameter.label_position, Some(label_position));
12933
12934        mock_ctx.close().await;
12935    }
12936
12937    #[tokio::test(flavor = "multi_thread")]
12938    async fn test_edit_diameter_constraint_label_position() {
12939        let initial_source = "\
12940sketch(on = XY) {
12941  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12942  diameter(arc1) == 10mm
12943}
12944";
12945
12946        let program = Program::parse(initial_source).unwrap().0.unwrap();
12947        let mut frontend = FrontendState::new();
12948        let mock_ctx = ExecutorContext::new_mock(None).await;
12949        let version = Version(0);
12950
12951        frontend.program = program.clone();
12952        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12953        frontend.update_state_after_exec(outcome, true);
12954        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12955        let sketch_id = sketch_object.id;
12956        let sketch = expect_sketch(sketch_object);
12957        let constraint_id = sketch.constraints[0];
12958        let label_position = Point2d {
12959            x: Number {
12960                value: 10.0,
12961                units: NumericSuffix::Mm,
12962            },
12963            y: Number {
12964                value: 11.0,
12965                units: NumericSuffix::Mm,
12966            },
12967        };
12968
12969        let (src_delta, scene_delta) = frontend
12970            .edit_distance_constraint_label_position(
12971                &mock_ctx,
12972                version,
12973                sketch_id,
12974                constraint_id,
12975                label_position.clone(),
12976                vec![],
12977            )
12978            .await
12979            .unwrap();
12980        insta::assert_snapshot!("test_edit_diameter_constraint_label_position", src_delta.text.as_str());
12981
12982        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12983        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12984            panic!("Expected constraint object");
12985        };
12986        let Constraint::Diameter(diameter) = constraint else {
12987            panic!("Expected diameter constraint");
12988        };
12989        assert_eq!(diameter.label_position, Some(label_position));
12990
12991        mock_ctx.close().await;
12992    }
12993
12994    #[tokio::test(flavor = "multi_thread")]
12995    async fn test_diameter_error_cases() {
12996        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12997        let mock_ctx = ExecutorContext::new_mock(None).await;
12998        let version = Version(0);
12999
13000        // Test: Single point should error
13001        let initial_source_point = "\
13002sketch(on = XY) {
13003  point(at = [var 1, var 2])
13004}
13005";
13006        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
13007        let mut frontend_point = FrontendState::new();
13008        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
13009        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
13010        let sketch_id_point = sketch_object_point.id;
13011        let sketch_point = expect_sketch(sketch_object_point);
13012        let point_id = *sketch_point.segments.first().unwrap();
13013
13014        let constraint_point = Constraint::Diameter(Diameter {
13015            arc: point_id,
13016            diameter: Number {
13017                value: 10.0,
13018                units: NumericSuffix::Mm,
13019            },
13020            label_position: None,
13021            source: Default::default(),
13022        });
13023        let result_point = frontend_point
13024            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
13025            .await;
13026        assert!(result_point.is_err(), "Single point should error for diameter");
13027
13028        // Test: Single line segment should error (only arc segments supported)
13029        let initial_source_line = "\
13030sketch(on = XY) {
13031  line(start = [var 1, var 2], end = [var 3, var 4])
13032}
13033";
13034        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
13035        let mut frontend_line = FrontendState::new();
13036        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
13037        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
13038        let sketch_id_line = sketch_object_line.id;
13039        let sketch_line = expect_sketch(sketch_object_line);
13040        let line_id = *sketch_line.segments.first().unwrap();
13041
13042        let constraint_line = Constraint::Diameter(Diameter {
13043            arc: line_id,
13044            diameter: Number {
13045                value: 10.0,
13046                units: NumericSuffix::Mm,
13047            },
13048            label_position: None,
13049            source: Default::default(),
13050        });
13051        let result_line = frontend_line
13052            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
13053            .await;
13054        assert!(result_line.is_err(), "Single line segment should error for diameter");
13055
13056        ctx.close().await;
13057        mock_ctx.close().await;
13058    }
13059
13060    #[tokio::test(flavor = "multi_thread")]
13061    async fn test_line_horizontal() {
13062        let initial_source = "\
13063sketch(on = XY) {
13064  line(start = [var 1, var 2], end = [var 3, var 4])
13065}
13066";
13067
13068        let program = Program::parse(initial_source).unwrap().0.unwrap();
13069
13070        let mut frontend = FrontendState::new();
13071
13072        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13073        let mock_ctx = ExecutorContext::new_mock(None).await;
13074        let version = Version(0);
13075
13076        frontend.hack_set_program(&ctx, program).await.unwrap();
13077        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13078        let sketch_id = sketch_object.id;
13079        let sketch = expect_sketch(sketch_object);
13080        let line1_id = *sketch.segments.get(2).unwrap();
13081
13082        let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
13083        let (src_delta, scene_delta) = frontend
13084            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13085            .await
13086            .unwrap();
13087        insta::assert_snapshot!("test_line_horizontal", src_delta.text.as_str());
13088        assert_eq!(
13089            scene_delta.new_graph.objects.len(),
13090            6,
13091            "{:#?}",
13092            scene_delta.new_graph.objects
13093        );
13094
13095        ctx.close().await;
13096        mock_ctx.close().await;
13097    }
13098
13099    #[tokio::test(flavor = "multi_thread")]
13100    async fn test_control_point_spline_edge_horizontal() {
13101        let initial_source = "\
13102@settings(experimentalFeatures = allow)
13103splineSketch = sketch(on = XY) {
13104  controlPointSpline1 = controlPointSpline(points = [
13105    [var 0mm, var 0mm],
13106    [var 10mm, var 20mm],
13107    [var 20mm, var 0mm],
13108  ])
13109}
13110";
13111
13112        let program = Program::parse(initial_source).unwrap().0.unwrap();
13113
13114        let mut frontend = FrontendState::new();
13115
13116        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13117        let mock_ctx = ExecutorContext::new_mock(None).await;
13118        let version = Version(0);
13119
13120        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13121        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13122        let sketch_id = sketch_object.id;
13123        let sketch = expect_sketch(sketch_object);
13124        let spline_id = sketch
13125            .segments
13126            .iter()
13127            .copied()
13128            .find(|seg_id| {
13129                matches!(
13130                    &frontend.scene_graph.objects[seg_id.0].kind,
13131                    ObjectKind::Segment {
13132                        segment: Segment::ControlPointSpline(_)
13133                    }
13134                )
13135            })
13136            .expect("Expected a control point spline segment in sketch");
13137        let edge_id = frontend
13138            .scene_graph
13139            .objects
13140            .iter()
13141            .find_map(|obj| match &obj.kind {
13142                ObjectKind::Segment {
13143                    segment: Segment::Line(line),
13144                } if line.owner == Some(spline_id) => Some(obj.id),
13145                _ => None,
13146            })
13147            .expect("Expected an owned control-polygon edge");
13148
13149        let constraint = Constraint::Horizontal(Horizontal::Line { line: edge_id });
13150        let (src_delta, _) = frontend
13151            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13152            .await
13153            .unwrap();
13154        assert!(
13155            src_delta.text.contains("horizontal(controlPointSpline1.edges[0])"),
13156            "Expected horizontal constraint on spline edge, got: {}",
13157            src_delta.text
13158        );
13159
13160        ctx.close().await;
13161        mock_ctx.close().await;
13162    }
13163
13164    #[tokio::test(flavor = "multi_thread")]
13165    async fn test_control_point_spline_edge_angle() {
13166        let initial_source = "\
13167@settings(experimentalFeatures = allow)
13168splineSketch = sketch(on = XY) {
13169  controlPointSpline1 = controlPointSpline(points = [
13170    [var 0mm, var 0mm],
13171    [var 10mm, var 20mm],
13172    [var 20mm, var 0mm],
13173  ])
13174
13175  line1 = line(start = [var 40mm, var 0mm], end = [var 60mm, var 10mm])
13176}
13177";
13178
13179        let program = Program::parse(initial_source).unwrap().0.unwrap();
13180
13181        let mut frontend = FrontendState::new();
13182
13183        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13184        let mock_ctx = ExecutorContext::new_mock(None).await;
13185        let version = Version(0);
13186
13187        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13188        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13189        let sketch_id = sketch_object.id;
13190        let sketch = expect_sketch(sketch_object);
13191        let spline_id = sketch
13192            .segments
13193            .iter()
13194            .copied()
13195            .find(|seg_id| {
13196                matches!(
13197                    &frontend.scene_graph.objects[seg_id.0].kind,
13198                    ObjectKind::Segment {
13199                        segment: Segment::ControlPointSpline(_)
13200                    }
13201                )
13202            })
13203            .expect("Expected a control point spline segment in sketch");
13204        let edge_id = frontend
13205            .scene_graph
13206            .objects
13207            .iter()
13208            .find_map(|obj| match &obj.kind {
13209                ObjectKind::Segment {
13210                    segment: Segment::Line(line),
13211                } if line.owner == Some(spline_id) => Some(obj.id),
13212                _ => None,
13213            })
13214            .expect("Expected an owned control-polygon edge");
13215        let line1_id = frontend
13216            .scene_graph
13217            .objects
13218            .iter()
13219            .find_map(|obj| match &obj.kind {
13220                ObjectKind::Segment {
13221                    segment: Segment::Line(line),
13222                } if line.owner.is_none() && obj.label == "line1" => Some(obj.id),
13223                _ => None,
13224            })
13225            .or_else(|| {
13226                sketch.segments.iter().copied().find(|seg_id| {
13227                    matches!(
13228                        &frontend.scene_graph.objects[seg_id.0].kind,
13229                        ObjectKind::Segment {
13230                            segment: Segment::Line(line),
13231                        } if line.owner.is_none()
13232                    )
13233                })
13234            })
13235            .expect("Expected a standalone line segment in sketch");
13236
13237        let constraint = Constraint::Angle(Angle {
13238            lines: vec![line1_id, edge_id],
13239            angle: Number {
13240                value: 30.0,
13241                units: NumericSuffix::Deg,
13242            },
13243            sector: None,
13244            inverse: None,
13245            label_position: None,
13246            source: Default::default(),
13247        });
13248        let (src_delta, _) = frontend
13249            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13250            .await
13251            .unwrap();
13252        assert!(
13253            src_delta
13254                .text
13255                .contains("angle([line1, controlPointSpline1.edges[0]]) == 30deg"),
13256            "Expected angle constraint on spline edge, got: {}",
13257            src_delta.text
13258        );
13259
13260        ctx.close().await;
13261        mock_ctx.close().await;
13262    }
13263
13264    #[tokio::test(flavor = "multi_thread")]
13265    async fn test_ui_scene_graph_hides_same_spline_coincident_constraints() {
13266        let initial_source = "\
13267@settings(experimentalFeatures = allow)
13268splineSketch = sketch(on = XY) {
13269  spline1 = controlPointSpline(points = [
13270    [var 0mm, var 0mm],
13271    [var 10mm, var 20mm],
13272    [var 20mm, var 0mm],
13273  ])
13274  line1 = line(start = [var 0mm, var 0mm], end = [var -10mm, var 0mm])
13275  coincident([spline1.controls[1], spline1.edges[0]])
13276  coincident([spline1.controls[0], line1])
13277}
13278";
13279
13280        let program = Program::parse(initial_source).unwrap().0.unwrap();
13281
13282        let mut frontend = FrontendState::new();
13283
13284        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13285        let mock_ctx = ExecutorContext::new_mock(None).await;
13286
13287        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13288
13289        let ui_scene_graph = frontend.scene_graph_for_ui();
13290        let sketch_object = find_first_sketch_object(&ui_scene_graph).unwrap();
13291        let sketch = expect_sketch(sketch_object);
13292
13293        assert_eq!(
13294            sketch.constraints.len(),
13295            1,
13296            "Expected only the external coincident constraint to remain visible in the UI scene graph"
13297        );
13298
13299        let visible_constraints = ui_scene_graph
13300            .objects
13301            .iter()
13302            .filter_map(|object| match &object.kind {
13303                ObjectKind::Constraint {
13304                    constraint: Constraint::Coincident(coincident),
13305                } => Some(coincident.clone()),
13306                _ => None,
13307            })
13308            .collect::<Vec<_>>();
13309
13310        assert_eq!(
13311            visible_constraints.len(),
13312            1,
13313            "Expected only one coincident constraint object in the UI scene graph"
13314        );
13315        assert_eq!(
13316            visible_constraints[0].get_segments().len(),
13317            2,
13318            "Expected the remaining visible coincident constraint to reference two segments"
13319        );
13320
13321        ctx.close().await;
13322        mock_ctx.close().await;
13323    }
13324
13325    #[tokio::test(flavor = "multi_thread")]
13326    async fn test_edit_control_point_spline_can_append_control_point() {
13327        let initial_source = "\
13328@settings(experimentalFeatures = allow)
13329splineSketch = sketch(on = XY) {
13330  controlPointSpline(points = [
13331    [var 0mm, var 0mm],
13332    [var 10mm, var 20mm],
13333    [var 20mm, var 0mm],
13334  ])
13335}
13336";
13337
13338        let program = Program::parse(initial_source).unwrap().0.unwrap();
13339
13340        let mut frontend = FrontendState::new();
13341
13342        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13343        let mock_ctx = ExecutorContext::new_mock(None).await;
13344        let version = Version(0);
13345
13346        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13347        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13348        let sketch_id = sketch_object.id;
13349        let sketch = expect_sketch(sketch_object);
13350        let spline_id = sketch
13351            .segments
13352            .iter()
13353            .copied()
13354            .find(|seg_id| {
13355                matches!(
13356                    &frontend.scene_graph.objects[seg_id.0].kind,
13357                    ObjectKind::Segment {
13358                        segment: Segment::ControlPointSpline(_)
13359                    }
13360                )
13361            })
13362            .expect("Expected a control point spline segment in sketch");
13363
13364        let ctor = ControlPointSplineCtor {
13365            points: vec![
13366                Point2d {
13367                    x: Expr::Var(Number {
13368                        value: 0.0,
13369                        units: NumericSuffix::Mm,
13370                    }),
13371                    y: Expr::Var(Number {
13372                        value: 0.0,
13373                        units: NumericSuffix::Mm,
13374                    }),
13375                },
13376                Point2d {
13377                    x: Expr::Var(Number {
13378                        value: 10.0,
13379                        units: NumericSuffix::Mm,
13380                    }),
13381                    y: Expr::Var(Number {
13382                        value: 20.0,
13383                        units: NumericSuffix::Mm,
13384                    }),
13385                },
13386                Point2d {
13387                    x: Expr::Var(Number {
13388                        value: 20.0,
13389                        units: NumericSuffix::Mm,
13390                    }),
13391                    y: Expr::Var(Number {
13392                        value: 0.0,
13393                        units: NumericSuffix::Mm,
13394                    }),
13395                },
13396                Point2d {
13397                    x: Expr::Var(Number {
13398                        value: 30.0,
13399                        units: NumericSuffix::Mm,
13400                    }),
13401                    y: Expr::Var(Number {
13402                        value: 10.0,
13403                        units: NumericSuffix::Mm,
13404                    }),
13405                },
13406            ],
13407            construction: None,
13408        };
13409
13410        let segments = vec![ExistingSegmentCtor {
13411            id: spline_id,
13412            ctor: SegmentCtor::ControlPointSpline(ctor),
13413        }];
13414        let (src_delta, scene_delta) = frontend
13415            .edit_segments(&mock_ctx, version, sketch_id, segments)
13416            .await
13417            .unwrap();
13418
13419        assert!(
13420            src_delta.text.contains("[var 30mm, var 10mm]"),
13421            "Expected appended spline control point in source, got: {}",
13422            src_delta.text
13423        );
13424
13425        assert!(
13426            scene_delta.invalidates_ids,
13427            "Expected appending a spline control point to invalidate ids"
13428        );
13429        let updated_spline = scene_delta
13430            .new_graph
13431            .objects
13432            .iter()
13433            .find_map(|obj| match &obj.kind {
13434                ObjectKind::Segment {
13435                    segment: Segment::ControlPointSpline(updated_spline),
13436                } if updated_spline.controls.len() == 4 => Some(updated_spline),
13437                _ => None,
13438            })
13439            .expect("Expected edited scene graph to contain a four-point control point spline");
13440        assert_eq!(
13441            updated_spline.controls.len(),
13442            4,
13443            "Expected edited spline to expose four control points"
13444        );
13445
13446        ctx.close().await;
13447        mock_ctx.close().await;
13448    }
13449
13450    #[tokio::test(flavor = "multi_thread")]
13451    async fn test_line_vertical() {
13452        let initial_source = "\
13453sketch(on = XY) {
13454  line(start = [var 1, var 2], end = [var 3, var 4])
13455}
13456";
13457
13458        let program = Program::parse(initial_source).unwrap().0.unwrap();
13459
13460        let mut frontend = FrontendState::new();
13461
13462        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13463        let mock_ctx = ExecutorContext::new_mock(None).await;
13464        let version = Version(0);
13465
13466        frontend.hack_set_program(&ctx, program).await.unwrap();
13467        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13468        let sketch_id = sketch_object.id;
13469        let sketch = expect_sketch(sketch_object);
13470        let line1_id = *sketch.segments.get(2).unwrap();
13471
13472        let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
13473        let (src_delta, scene_delta) = frontend
13474            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13475            .await
13476            .unwrap();
13477        insta::assert_snapshot!("test_line_vertical", src_delta.text.as_str());
13478        assert_eq!(
13479            scene_delta.new_graph.objects.len(),
13480            6,
13481            "{:#?}",
13482            scene_delta.new_graph.objects
13483        );
13484
13485        ctx.close().await;
13486        mock_ctx.close().await;
13487    }
13488
13489    #[tokio::test(flavor = "multi_thread")]
13490    async fn test_points_vertical() {
13491        let initial_source = "\
13492sketch001 = sketch(on = XY) {
13493  p0 = point(at = [var -2.23mm, var 3.1mm])
13494  pf = point(at = [4, 4])
13495}
13496";
13497
13498        let program = Program::parse(initial_source).unwrap().0.unwrap();
13499
13500        let mut frontend = FrontendState::new();
13501
13502        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13503        let mock_ctx = ExecutorContext::new_mock(None).await;
13504        let version = Version(0);
13505
13506        frontend.hack_set_program(&ctx, program).await.unwrap();
13507        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13508        let sketch_id = sketch_object.id;
13509        let sketch = expect_sketch(sketch_object);
13510        let point_ids = vec![
13511            sketch.segments.first().unwrap().to_owned(),
13512            sketch.segments.get(1).unwrap().to_owned(),
13513        ];
13514
13515        let constraint = Constraint::Vertical(Vertical::Points {
13516            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
13517        });
13518        let (src_delta, scene_delta) = frontend
13519            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13520            .await
13521            .unwrap();
13522        insta::assert_snapshot!("test_points_vertical", src_delta.text.as_str());
13523        assert_eq!(
13524            scene_delta.new_graph.objects.len(),
13525            5,
13526            "{:#?}",
13527            scene_delta.new_graph.objects
13528        );
13529
13530        ctx.close().await;
13531        mock_ctx.close().await;
13532    }
13533
13534    #[tokio::test(flavor = "multi_thread")]
13535    async fn test_points_horizontal() {
13536        let initial_source = "\
13537sketch001 = sketch(on = XY) {
13538  p0 = point(at = [var -2.23mm, var 3.1mm])
13539  pf = point(at = [4, 4])
13540}
13541";
13542
13543        let program = Program::parse(initial_source).unwrap().0.unwrap();
13544
13545        let mut frontend = FrontendState::new();
13546
13547        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13548        let mock_ctx = ExecutorContext::new_mock(None).await;
13549        let version = Version(0);
13550
13551        frontend.hack_set_program(&ctx, program).await.unwrap();
13552        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13553        let sketch_id = sketch_object.id;
13554        let sketch = expect_sketch(sketch_object);
13555        let point_ids = vec![
13556            sketch.segments.first().unwrap().to_owned(),
13557            sketch.segments.get(1).unwrap().to_owned(),
13558        ];
13559
13560        let constraint = Constraint::Horizontal(Horizontal::Points {
13561            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
13562        });
13563        let (src_delta, scene_delta) = frontend
13564            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13565            .await
13566            .unwrap();
13567        insta::assert_snapshot!("test_points_horizontal", src_delta.text.as_str());
13568        assert_eq!(
13569            scene_delta.new_graph.objects.len(),
13570            5,
13571            "{:#?}",
13572            scene_delta.new_graph.objects
13573        );
13574
13575        ctx.close().await;
13576        mock_ctx.close().await;
13577    }
13578
13579    #[tokio::test(flavor = "multi_thread")]
13580    async fn test_point_horizontal_with_origin() {
13581        let initial_source = "\
13582sketch001 = sketch(on = XY) {
13583  p0 = point(at = [var -2.23mm, var 3.1mm])
13584}
13585";
13586
13587        let program = Program::parse(initial_source).unwrap().0.unwrap();
13588
13589        let mut frontend = FrontendState::new();
13590
13591        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13592        let mock_ctx = ExecutorContext::new_mock(None).await;
13593        let version = Version(0);
13594
13595        frontend.hack_set_program(&ctx, program).await.unwrap();
13596        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13597        let sketch_id = sketch_object.id;
13598        let sketch = expect_sketch(sketch_object);
13599        let point_id = *sketch.segments.first().unwrap();
13600
13601        let constraint = Constraint::Horizontal(Horizontal::Points {
13602            points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
13603        });
13604        let (src_delta, scene_delta) = frontend
13605            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13606            .await
13607            .unwrap();
13608        insta::assert_snapshot!("test_point_horizontal_with_origin", src_delta.text.as_str());
13609        assert_eq!(
13610            scene_delta.new_graph.objects.len(),
13611            4,
13612            "{:#?}",
13613            scene_delta.new_graph.objects
13614        );
13615
13616        ctx.close().await;
13617        mock_ctx.close().await;
13618    }
13619
13620    #[tokio::test(flavor = "multi_thread")]
13621    async fn test_lines_equal_length() {
13622        let initial_source = "\
13623sketch(on = XY) {
13624  line(start = [var 1, var 2], end = [var 3, var 4])
13625  line(start = [var 5, var 6], end = [var 7, var 8])
13626}
13627";
13628
13629        let program = Program::parse(initial_source).unwrap().0.unwrap();
13630
13631        let mut frontend = FrontendState::new();
13632
13633        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13634        let mock_ctx = ExecutorContext::new_mock(None).await;
13635        let version = Version(0);
13636
13637        frontend.hack_set_program(&ctx, program).await.unwrap();
13638        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13639        let sketch_id = sketch_object.id;
13640        let sketch = expect_sketch(sketch_object);
13641        let line1_id = *sketch.segments.get(2).unwrap();
13642        let line2_id = *sketch.segments.get(5).unwrap();
13643
13644        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13645            lines: vec![line1_id, line2_id],
13646        });
13647        let (src_delta, scene_delta) = frontend
13648            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13649            .await
13650            .unwrap();
13651        insta::assert_snapshot!("test_lines_equal_length", src_delta.text.as_str());
13652        assert_eq!(
13653            scene_delta.new_graph.objects.len(),
13654            9,
13655            "{:#?}",
13656            scene_delta.new_graph.objects
13657        );
13658
13659        ctx.close().await;
13660        mock_ctx.close().await;
13661    }
13662
13663    #[tokio::test(flavor = "multi_thread")]
13664    async fn test_add_constraint_multi_line_equal_length() {
13665        let initial_source = "\
13666sketch(on = XY) {
13667  line(start = [var 1, var 2], end = [var 3, var 4])
13668  line(start = [var 5, var 6], end = [var 7, var 8])
13669  line(start = [var 9, var 10], end = [var 11, var 12])
13670}
13671";
13672
13673        let program = Program::parse(initial_source).unwrap().0.unwrap();
13674
13675        let mut frontend = FrontendState::new();
13676        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13677        let mock_ctx = ExecutorContext::new_mock(None).await;
13678        let version = Version(0);
13679
13680        frontend.hack_set_program(&ctx, program).await.unwrap();
13681        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13682        let sketch_id = sketch_object.id;
13683        let sketch = expect_sketch(sketch_object);
13684        let line1_id = *sketch.segments.get(2).unwrap();
13685        let line2_id = *sketch.segments.get(5).unwrap();
13686        let line3_id = *sketch.segments.get(8).unwrap();
13687
13688        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13689            lines: vec![line1_id, line2_id, line3_id],
13690        });
13691        let (src_delta, scene_delta) = frontend
13692            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13693            .await
13694            .unwrap();
13695        insta::assert_snapshot!("test_add_constraint_multi_line_equal_length", src_delta.text.as_str());
13696        let constraints = scene_delta
13697            .new_graph
13698            .objects
13699            .iter()
13700            .filter_map(|obj| {
13701                let ObjectKind::Constraint { constraint } = &obj.kind else {
13702                    return None;
13703                };
13704                Some(constraint)
13705            })
13706            .collect::<Vec<_>>();
13707
13708        assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
13709        let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
13710            panic!("expected equal length constraint, got {:?}", constraints[0]);
13711        };
13712        assert_eq!(lines_equal_length.lines.len(), 3);
13713
13714        ctx.close().await;
13715        mock_ctx.close().await;
13716    }
13717
13718    #[tokio::test(flavor = "multi_thread")]
13719    async fn test_lines_parallel() {
13720        let initial_source = "\
13721sketch(on = XY) {
13722  line(start = [var 1, var 2], end = [var 3, var 4])
13723  line(start = [var 5, var 6], end = [var 7, var 8])
13724}
13725";
13726
13727        let program = Program::parse(initial_source).unwrap().0.unwrap();
13728
13729        let mut frontend = FrontendState::new();
13730
13731        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13732        let mock_ctx = ExecutorContext::new_mock(None).await;
13733        let version = Version(0);
13734
13735        frontend.hack_set_program(&ctx, program).await.unwrap();
13736        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13737        let sketch_id = sketch_object.id;
13738        let sketch = expect_sketch(sketch_object);
13739        let line1_id = *sketch.segments.get(2).unwrap();
13740        let line2_id = *sketch.segments.get(5).unwrap();
13741
13742        let constraint = Constraint::Parallel(Parallel {
13743            lines: vec![line1_id, line2_id],
13744        });
13745        let (src_delta, scene_delta) = frontend
13746            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13747            .await
13748            .unwrap();
13749        insta::assert_snapshot!("test_lines_parallel", src_delta.text.as_str());
13750        assert_eq!(
13751            scene_delta.new_graph.objects.len(),
13752            9,
13753            "{:#?}",
13754            scene_delta.new_graph.objects
13755        );
13756
13757        ctx.close().await;
13758        mock_ctx.close().await;
13759    }
13760
13761    #[tokio::test(flavor = "multi_thread")]
13762    async fn test_lines_parallel_multiline() {
13763        let initial_source = "\
13764sketch(on = XY) {
13765  line(start = [var 1, var 2], end = [var 3, var 4])
13766  line(start = [var 5, var 6], end = [var 7, var 8])
13767  line(start = [var 9, var 10], end = [var 11, var 12])
13768}
13769";
13770
13771        let program = Program::parse(initial_source).unwrap().0.unwrap();
13772
13773        let mut frontend = FrontendState::new();
13774
13775        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13776        let mock_ctx = ExecutorContext::new_mock(None).await;
13777        let version = Version(0);
13778
13779        frontend.hack_set_program(&ctx, program).await.unwrap();
13780        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13781        let sketch_id = sketch_object.id;
13782        let sketch = expect_sketch(sketch_object);
13783        let line1_id = *sketch.segments.get(2).unwrap();
13784        let line2_id = *sketch.segments.get(5).unwrap();
13785        let line3_id = *sketch.segments.get(8).unwrap();
13786
13787        let constraint = Constraint::Parallel(Parallel {
13788            lines: vec![line1_id, line2_id, line3_id],
13789        });
13790        let (src_delta, scene_delta) = frontend
13791            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13792            .await
13793            .unwrap();
13794        insta::assert_snapshot!("test_lines_parallel_multiline", src_delta.text.as_str());
13795
13796        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
13797        let sketch = expect_sketch(sketch_object);
13798        assert_eq!(sketch.constraints.len(), 1);
13799
13800        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
13801        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
13802            panic!("Expected constraint object");
13803        };
13804        let Constraint::Parallel(parallel) = constraint else {
13805            panic!("Expected parallel constraint");
13806        };
13807        assert_eq!(parallel.lines.len(), 3);
13808
13809        ctx.close().await;
13810        mock_ctx.close().await;
13811    }
13812
13813    #[tokio::test(flavor = "multi_thread")]
13814    async fn test_lines_perpendicular() {
13815        let initial_source = "\
13816sketch(on = XY) {
13817  line(start = [var 1, var 2], end = [var 3, var 4])
13818  line(start = [var 5, var 6], end = [var 7, var 8])
13819}
13820";
13821
13822        let program = Program::parse(initial_source).unwrap().0.unwrap();
13823
13824        let mut frontend = FrontendState::new();
13825
13826        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13827        let mock_ctx = ExecutorContext::new_mock(None).await;
13828        let version = Version(0);
13829
13830        frontend.hack_set_program(&ctx, program).await.unwrap();
13831        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13832        let sketch_id = sketch_object.id;
13833        let sketch = expect_sketch(sketch_object);
13834        let line1_id = *sketch.segments.get(2).unwrap();
13835        let line2_id = *sketch.segments.get(5).unwrap();
13836
13837        let constraint = Constraint::Perpendicular(Perpendicular {
13838            lines: vec![line1_id, line2_id],
13839        });
13840        let (src_delta, scene_delta) = frontend
13841            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13842            .await
13843            .unwrap();
13844        insta::assert_snapshot!("test_lines_perpendicular", src_delta.text.as_str());
13845        assert_eq!(
13846            scene_delta.new_graph.objects.len(),
13847            9,
13848            "{:#?}",
13849            scene_delta.new_graph.objects
13850        );
13851
13852        ctx.close().await;
13853        mock_ctx.close().await;
13854    }
13855
13856    #[tokio::test(flavor = "multi_thread")]
13857    async fn test_lines_angle() {
13858        let initial_source = "\
13859sketch(on = XY) {
13860  line(start = [var 1, var 2], end = [var 3, var 4])
13861  line(start = [var 5, var 6], end = [var 7, var 8])
13862}
13863";
13864
13865        let program = Program::parse(initial_source).unwrap().0.unwrap();
13866
13867        let mut frontend = FrontendState::new();
13868
13869        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13870        let mock_ctx = ExecutorContext::new_mock(None).await;
13871        let version = Version(0);
13872
13873        frontend.hack_set_program(&ctx, program).await.unwrap();
13874        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13875        let sketch_id = sketch_object.id;
13876        let sketch = expect_sketch(sketch_object);
13877        let line1_id = *sketch.segments.get(2).unwrap();
13878        let line2_id = *sketch.segments.get(5).unwrap();
13879
13880        let constraint = Constraint::Angle(Angle {
13881            lines: vec![line1_id, line2_id],
13882            angle: Number {
13883                value: 30.0,
13884                units: NumericSuffix::Deg,
13885            },
13886            sector: None,
13887            inverse: None,
13888            label_position: None,
13889            source: Default::default(),
13890        });
13891        let (src_delta, scene_delta) = frontend
13892            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13893            .await
13894            .unwrap();
13895        insta::assert_snapshot!("test_lines_angle", src_delta.text.as_str());
13896        assert_eq!(
13897            scene_delta.new_graph.objects.len(),
13898            9,
13899            "{:#?}",
13900            scene_delta.new_graph.objects
13901        );
13902
13903        ctx.close().await;
13904        mock_ctx.close().await;
13905    }
13906
13907    #[tokio::test(flavor = "multi_thread")]
13908    async fn test_lines_angle_with_sector_uses_angle_dimension() {
13909        let initial_source = "\
13910sketch(on = XY) {
13911  line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
13912  line(start = [var 0mm, var 0mm], end = [var 0mm, var 4mm])
13913}
13914";
13915
13916        let program = Program::parse(initial_source).unwrap().0.unwrap();
13917
13918        let mut frontend = FrontendState::new();
13919
13920        let mock_ctx = ExecutorContext::new_mock(None).await;
13921        let version = Version(0);
13922
13923        frontend.program = program.clone();
13924        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13925        frontend.update_state_after_exec(outcome, true);
13926        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13927        let sketch_id = sketch_object.id;
13928        let sketch = expect_sketch(sketch_object);
13929        let line1_id = *sketch.segments.get(2).unwrap();
13930        let line2_id = *sketch.segments.get(5).unwrap();
13931
13932        let constraint = Constraint::Angle(Angle {
13933            lines: vec![line1_id, line2_id],
13934            angle: Number {
13935                value: 270.0,
13936                units: NumericSuffix::Deg,
13937            },
13938            sector: Some(1),
13939            inverse: Some(true),
13940            label_position: Some(Point2d {
13941                x: Number {
13942                    value: -0.73,
13943                    units: NumericSuffix::Mm,
13944                },
13945                y: Number {
13946                    value: 0.75,
13947                    units: NumericSuffix::Mm,
13948                },
13949            }),
13950            source: Default::default(),
13951        });
13952        let (src_delta, _) = frontend
13953            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13954            .await
13955            .unwrap();
13956        assert_eq!(
13957            src_delta.text.as_str(),
13958            "\
13959sketch(on = XY) {
13960  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
13961  line2 = line(start = [var 0mm, var 0mm], end = [var 0mm, var 4mm])
13962  angleDimension(
13963  lines = [line1, line2],
13964  sector = 1,
13965  inverse = true,
13966  labelPosition = [-0.73mm, 0.75mm],
13967) == 270deg
13968}
13969"
13970        );
13971
13972        mock_ctx.close().await;
13973    }
13974
13975    #[tokio::test(flavor = "multi_thread")]
13976    async fn test_segments_tangent() {
13977        let initial_source = "\
13978sketch(on = XY) {
13979  line(start = [var 1, var 2], end = [var 3, var 4])
13980  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13981}
13982";
13983
13984        let program = Program::parse(initial_source).unwrap().0.unwrap();
13985
13986        let mut frontend = FrontendState::new();
13987
13988        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13989        let mock_ctx = ExecutorContext::new_mock(None).await;
13990        let version = Version(0);
13991
13992        frontend.hack_set_program(&ctx, program).await.unwrap();
13993        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13994        let sketch_id = sketch_object.id;
13995        let sketch = expect_sketch(sketch_object);
13996        let line1_id = *sketch.segments.get(2).unwrap();
13997        let arc1_id = *sketch.segments.get(6).unwrap();
13998
13999        let constraint = Constraint::Tangent(Tangent {
14000            input: vec![line1_id, arc1_id],
14001        });
14002        let (src_delta, scene_delta) = frontend
14003            .add_constraint(&mock_ctx, version, sketch_id, constraint)
14004            .await
14005            .unwrap();
14006        insta::assert_snapshot!("test_segments_tangent", src_delta.text.as_str());
14007        assert_eq!(
14008            scene_delta.new_graph.objects.len(),
14009            10,
14010            "{:#?}",
14011            scene_delta.new_graph.objects
14012        );
14013
14014        ctx.close().await;
14015        mock_ctx.close().await;
14016    }
14017
14018    #[tokio::test(flavor = "multi_thread")]
14019    async fn test_point_midpoint() {
14020        let initial_source = "\
14021sketch(on = XY) {
14022  point(at = [var 1, var 1])
14023  line(start = [var 0, var 0], end = [var 6, var 4])
14024}
14025";
14026
14027        let program = Program::parse(initial_source).unwrap().0.unwrap();
14028
14029        let mut frontend = FrontendState::new();
14030
14031        let ctx = ExecutorContext::new_mock(None).await;
14032        let version = Version(0);
14033
14034        frontend.program = program.clone();
14035        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14036        frontend.update_state_after_exec(outcome, true);
14037        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14038        let sketch_id = sketch_object.id;
14039        let sketch = expect_sketch(sketch_object);
14040        let point_id = *sketch.segments.first().unwrap();
14041        let line_id = *sketch.segments.get(3).unwrap();
14042
14043        let constraint = Constraint::Midpoint(Midpoint {
14044            point: ConstraintSegment::from(point_id),
14045            segment: line_id,
14046        });
14047        let (src_delta, scene_delta) = frontend
14048            .add_constraint(&ctx, version, sketch_id, constraint)
14049            .await
14050            .unwrap();
14051        insta::assert_snapshot!("test_point_midpoint", src_delta.text.as_str());
14052        assert_eq!(
14053            scene_delta.new_graph.objects.len(),
14054            7,
14055            "{:#?}",
14056            scene_delta.new_graph.objects
14057        );
14058
14059        ctx.close().await;
14060    }
14061
14062    #[tokio::test(flavor = "multi_thread")]
14063    async fn test_segments_symmetric() {
14064        let initial_source = "\
14065sketch(on = XY) {
14066  line(start = [var 0, var 0], end = [var 0, var 4])
14067  line(start = [var 4, var 0], end = [var 4, var 4])
14068  line(start = [var 2, var -1], end = [var 2, var 5])
14069}
14070";
14071
14072        let program = Program::parse(initial_source).unwrap().0.unwrap();
14073
14074        let mut frontend = FrontendState::new();
14075
14076        let ctx = ExecutorContext::new_mock(None).await;
14077        let version = Version(0);
14078
14079        frontend.program = program.clone();
14080        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14081        frontend.update_state_after_exec(outcome, true);
14082        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14083        let sketch_id = sketch_object.id;
14084        let sketch = expect_sketch(sketch_object);
14085        let line1_id = *sketch.segments.get(2).unwrap();
14086        let line2_id = *sketch.segments.get(5).unwrap();
14087        let axis_id = *sketch.segments.get(8).unwrap();
14088
14089        let constraint = Constraint::Symmetric(Symmetric {
14090            input: vec![line1_id, line2_id],
14091            axis: axis_id,
14092        });
14093        let (src_delta, scene_delta) = frontend
14094            .add_constraint(&ctx, version, sketch_id, constraint)
14095            .await
14096            .unwrap();
14097        insta::assert_snapshot!("test_segments_symmetric", src_delta.text.as_str());
14098        assert_eq!(
14099            scene_delta.new_graph.objects.len(),
14100            12,
14101            "{:#?}",
14102            scene_delta.new_graph.objects
14103        );
14104
14105        ctx.close().await;
14106    }
14107
14108    #[tokio::test(flavor = "multi_thread")]
14109    async fn test_point_arc_midpoint() {
14110        let initial_source = "\
14111sketch(on = XY) {
14112  point(at = [var 6, var 3])
14113  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
14114}
14115";
14116
14117        let program = Program::parse(initial_source).unwrap().0.unwrap();
14118
14119        let mut frontend = FrontendState::new();
14120
14121        let ctx = ExecutorContext::new_mock(None).await;
14122        let version = Version(0);
14123
14124        frontend.program = program.clone();
14125        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14126        frontend.update_state_after_exec(outcome, true);
14127        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14128        let sketch_id = sketch_object.id;
14129        let sketch = expect_sketch(sketch_object);
14130        let point_id = *sketch.segments.first().unwrap();
14131        let arc_id = *sketch.segments.get(4).unwrap();
14132
14133        let constraint = Constraint::Midpoint(Midpoint {
14134            point: ConstraintSegment::from(point_id),
14135            segment: arc_id,
14136        });
14137        let (src_delta, scene_delta) = frontend
14138            .add_constraint(&ctx, version, sketch_id, constraint)
14139            .await
14140            .unwrap();
14141        insta::assert_snapshot!("test_point_arc_midpoint", src_delta.text.as_str());
14142        assert_eq!(
14143            scene_delta.new_graph.objects.len(),
14144            8,
14145            "{:#?}",
14146            scene_delta.new_graph.objects
14147        );
14148
14149        ctx.close().await;
14150    }
14151
14152    #[tokio::test(flavor = "multi_thread")]
14153    async fn test_origin_line_midpoint() {
14154        let initial_source = "\
14155sketch(on = XY) {
14156  line(start = [var 0, var 0], end = [var 6, var 4])
14157}
14158";
14159
14160        let program = Program::parse(initial_source).unwrap().0.unwrap();
14161
14162        let mut frontend = FrontendState::new();
14163
14164        let ctx = ExecutorContext::new_mock(None).await;
14165        let version = Version(0);
14166
14167        frontend.program = program.clone();
14168        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14169        frontend.update_state_after_exec(outcome, true);
14170        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14171        let sketch_id = sketch_object.id;
14172        let sketch = expect_sketch(sketch_object);
14173        let line_id = *sketch.segments.get(2).unwrap();
14174
14175        let constraint = Constraint::Midpoint(Midpoint {
14176            point: ConstraintSegment::ORIGIN,
14177            segment: line_id,
14178        });
14179        let (src_delta, scene_delta) = frontend
14180            .add_constraint(&ctx, version, sketch_id, constraint)
14181            .await
14182            .unwrap();
14183        insta::assert_snapshot!("test_origin_line_midpoint", src_delta.text.as_str());
14184        assert_eq!(
14185            scene_delta.new_graph.objects.len(),
14186            6,
14187            "{:#?}",
14188            scene_delta.new_graph.objects
14189        );
14190
14191        ctx.close().await;
14192    }
14193
14194    #[tokio::test(flavor = "multi_thread")]
14195    async fn test_origin_arc_midpoint() {
14196        let initial_source = "\
14197sketch(on = XY) {
14198  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
14199}
14200";
14201
14202        let program = Program::parse(initial_source).unwrap().0.unwrap();
14203
14204        let mut frontend = FrontendState::new();
14205
14206        let ctx = ExecutorContext::new_mock(None).await;
14207        let version = Version(0);
14208
14209        frontend.program = program.clone();
14210        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14211        frontend.update_state_after_exec(outcome, true);
14212        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14213        let sketch_id = sketch_object.id;
14214        let sketch = expect_sketch(sketch_object);
14215        let arc_id = *sketch.segments.get(3).unwrap();
14216
14217        let constraint = Constraint::Midpoint(Midpoint {
14218            point: ConstraintSegment::ORIGIN,
14219            segment: arc_id,
14220        });
14221        let (src_delta, scene_delta) = frontend
14222            .add_constraint(&ctx, version, sketch_id, constraint)
14223            .await
14224            .unwrap();
14225        insta::assert_snapshot!("test_origin_arc_midpoint", src_delta.text.as_str());
14226        assert_eq!(
14227            scene_delta.new_graph.objects.len(),
14228            7,
14229            "{:#?}",
14230            scene_delta.new_graph.objects
14231        );
14232
14233        ctx.close().await;
14234    }
14235
14236    #[tokio::test(flavor = "multi_thread")]
14237    async fn test_segments_symmetric_arcs() {
14238        let initial_source = "\
14239sketch(on = XY) {
14240  arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
14241  arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
14242  line(start = [var 0, var -10], end = [var 0, var 10])
14243}
14244";
14245
14246        let program = Program::parse(initial_source).unwrap().0.unwrap();
14247
14248        let mut frontend = FrontendState::new();
14249
14250        let ctx = ExecutorContext::new_mock(None).await;
14251        let version = Version(0);
14252
14253        frontend.program = program.clone();
14254        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14255        frontend.update_state_after_exec(outcome, true);
14256        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14257        let sketch_id = sketch_object.id;
14258        let sketch = expect_sketch(sketch_object);
14259        let arc1_id = *sketch.segments.get(3).unwrap();
14260        let arc2_id = *sketch.segments.get(7).unwrap();
14261        let axis_id = *sketch.segments.get(10).unwrap();
14262
14263        let constraint = Constraint::Symmetric(Symmetric {
14264            input: vec![arc1_id, arc2_id],
14265            axis: axis_id,
14266        });
14267        let (src_delta, scene_delta) = frontend
14268            .add_constraint(&ctx, version, sketch_id, constraint)
14269            .await
14270            .unwrap();
14271        insta::assert_snapshot!("test_segments_symmetric_arcs", src_delta.text.as_str());
14272        assert_eq!(
14273            scene_delta.new_graph.objects.len(),
14274            14,
14275            "{:#?}",
14276            scene_delta.new_graph.objects
14277        );
14278
14279        ctx.close().await;
14280    }
14281
14282    #[tokio::test(flavor = "multi_thread")]
14283    async fn test_sketch_on_face_simple() {
14284        let initial_source = "\
14285len = 2mm
14286cube = startSketchOn(XY)
14287  |> startProfile(at = [0, 0])
14288  |> line(end = [len, 0], tag = $side)
14289  |> line(end = [0, len])
14290  |> line(end = [-len, 0])
14291  |> line(end = [0, -len])
14292  |> close()
14293  |> extrude(length = len)
14294
14295face = faceOf(cube, face = side)
14296";
14297
14298        let program = Program::parse(initial_source).unwrap().0.unwrap();
14299
14300        let mut frontend = FrontendState::new();
14301
14302        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14303        let mock_ctx = ExecutorContext::new_mock(None).await;
14304        let version = Version(0);
14305
14306        frontend.hack_set_program(&ctx, program).await.unwrap();
14307        let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
14308        let face_id = face_object.id;
14309
14310        let sketch_args = SketchCtor {
14311            on: Plane::Object(face_id),
14312        };
14313        let (_src_delta, scene_delta, sketch_id) = frontend
14314            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14315            .await
14316            .unwrap();
14317        assert_eq!(sketch_id, ObjectId(2));
14318        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14319        let sketch_object = &scene_delta.new_graph.objects[2];
14320        assert_eq!(sketch_object.id, ObjectId(2));
14321        assert_eq!(
14322            sketch_object.kind,
14323            ObjectKind::Sketch(Sketch {
14324                args: SketchCtor {
14325                    on: Plane::Object(face_id),
14326                },
14327                plane: face_id,
14328                segments: vec![],
14329                constraints: vec![],
14330            })
14331        );
14332        assert_eq!(scene_delta.new_graph.objects.len(), 8);
14333
14334        ctx.close().await;
14335        mock_ctx.close().await;
14336    }
14337
14338    #[tokio::test(flavor = "multi_thread")]
14339    async fn test_new_sketch_on_primitive_index_face() {
14340        let initial_source = "\
14341@settings(kclVersion = 2.0)
14342
14343sketch001 = sketch(on = XY) {
14344  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
14345}
14346extrude001 = extrude(region(point = [0mm, 0mm], sketch = sketch001), length = 5, tagEnd = $capEnd001)
14347shell001 = shell(extrude001, faces = capEnd001, thickness = 1)";
14348        let program = Program::parse(initial_source).unwrap().0.unwrap();
14349        let ctx = ExecutorContext::new_mock(None).await;
14350        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14351        let solid_id = match outcome.variables.get("shell001") {
14352            Some(KclValueView::Solid { value }) => value.id,
14353            value => panic!("expected shell001 to be a solid, got {value:?}"),
14354        };
14355        let solid_references = solid_references_from_variables(&program.ast, &outcome.variables);
14356
14357        let mut ast = program.ast;
14358        let scene_graph = SceneGraph::empty(ProjectId(0), FileId(0), Version(0));
14359        let face_expr = sketch_on_ast_expr(
14360            &mut ast,
14361            &scene_graph,
14362            &solid_references,
14363            &Plane::PrimitiveFace(crate::frontend::api::PrimitiveFacePlane { solid_id, index: 6 }),
14364        )
14365        .unwrap();
14366        let face_decl = ast::VariableDeclaration::new(
14367            ast::VariableDeclarator::new("face001", face_expr),
14368            ast::ItemVisibility::Default,
14369            ast::VariableKind::Const,
14370        );
14371        ast.body
14372            .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
14373                face_decl,
14374            ))));
14375        let face_source = source_from_ast(&ast);
14376        let new_source = format!("{face_source}sketch002 = sketch(on = face001) {{\n}}\n");
14377        insta::assert_snapshot!("test_new_sketch_on_primitive_index_face", new_source);
14378
14379        let program = Program::parse(&new_source).unwrap().0.unwrap();
14380        ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14381        ctx.close().await;
14382    }
14383
14384    #[tokio::test(flavor = "multi_thread")]
14385    async fn test_sketch_on_wall_artifact_from_region_extrude() {
14386        let initial_source = "\
14387s = sketch(on = YZ) {
14388  line1 = line(start = [0, 0], end = [0, 1])
14389  line2 = line(start = [0, 1], end = [1, 1])
14390  line3 = line(start = [1, 1], end = [0, 0])
14391}
14392region001 = region(point = [0.1, 0.1], sketch = s)
14393extrude001 = extrude(region001, length = 5)
14394";
14395
14396        let program = Program::parse(initial_source).unwrap().0.unwrap();
14397
14398        let mut frontend = FrontendState::new();
14399        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14400        let version = Version(0);
14401
14402        frontend.hack_set_program(&ctx, program).await.unwrap();
14403        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
14404
14405        let sketch_args = SketchCtor {
14406            on: Plane::Object(wall_object_id),
14407        };
14408        let (src_delta, _scene_delta, _sketch_id) = frontend
14409            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14410            .await
14411            .unwrap();
14412        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
14413
14414        ctx.close().await;
14415    }
14416
14417    #[tokio::test(flavor = "multi_thread")]
14418    async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
14419        let initial_source = "\
14420sketch001 = sketch(on = YZ) {
14421  line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
14422  line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
14423  line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
14424  line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
14425  coincident([line1.end, line2.start])
14426  coincident([line2.end, line3.start])
14427  coincident([line3.end, line4.start])
14428  coincident([line4.end, line1.start])
14429  parallel([line2, line4])
14430  parallel([line3, line1])
14431  perpendicular([line1, line2])
14432  horizontal(line3)
14433  line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
14434}
14435region001 = region(point = [3.1, 3.74], sketch = sketch001)
14436extrude001 = extrude(region001, length = 5)
14437";
14438
14439        let program = Program::parse(initial_source).unwrap().0.unwrap();
14440
14441        let mut frontend = FrontendState::new();
14442        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14443        let version = Version(0);
14444
14445        frontend.hack_set_program(&ctx, program).await.unwrap();
14446        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
14447
14448        let sketch_args = SketchCtor {
14449            on: Plane::Object(wall_object_id),
14450        };
14451        let (src_delta, _scene_delta, _sketch_id) = frontend
14452            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14453            .await
14454            .unwrap();
14455        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
14456
14457        ctx.close().await;
14458    }
14459
14460    #[tokio::test(flavor = "multi_thread")]
14461    async fn test_new_sketch_on_multi_region_extrude_cap_indexes_selected_solid() {
14462        let initial_source = "\
14463@settings(kclVersion = 2.0)
14464
14465sketch001 = sketch(on = XY) {
14466  circle1 = circle(start = [var -1.51mm, var 1.31mm], center = [var -1.98mm, var 1.03mm])
14467  circle2 = circle(start = [var 2.98mm, var 2.37mm], center = [var 2.66mm, var 1.46mm])
14468}
14469hidden001 = hide(sketch001)
14470region001 = region(segments = [sketch001.circle2])
14471region002 = region(segments = [sketch001.circle1])
14472extrude001 = extrude([region001, region002], length = 5)
14473";
14474
14475        let program = Program::parse(initial_source).unwrap().0.unwrap();
14476        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14477        let version = Version(0);
14478
14479        for (solid_output_index, expected_face) in [
14480            (0, "faceOf(extrude001[0], face = END)"),
14481            (1, "faceOf(extrude001[1], face = END)"),
14482        ] {
14483            let mut frontend = FrontendState::new();
14484            frontend.hack_set_program(&ctx, program.clone()).await.unwrap();
14485            let cap_object_id = find_cap_object_id_with_solid_output_index(
14486                &frontend.scene_graph,
14487                crate::frontend::api::CapKind::End,
14488                solid_output_index,
14489            )
14490            .unwrap_or_else(|| panic!("expected an end cap object for solid output index {solid_output_index}"));
14491
14492            let sketch_args = SketchCtor {
14493                on: Plane::Object(cap_object_id),
14494            };
14495            let (src_delta, _scene_delta, _sketch_id) = frontend
14496                .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14497                .await
14498                .unwrap();
14499
14500            assert!(
14501                src_delta.text.contains(expected_face),
14502                "expected `{expected_face}` in:\n{}",
14503                src_delta.text
14504            );
14505            assert!(!src_delta.text.contains("faceOf(extrude001, face = END)"));
14506        }
14507
14508        ctx.close().await;
14509    }
14510
14511    #[tokio::test(flavor = "multi_thread")]
14512    async fn test_new_sketch_on_multi_region_extrude_wall_indexes_selected_solid() {
14513        let initial_source = "\
14514@settings(kclVersion = 2.0)
14515
14516sketch001 = sketch(on = XY) {
14517  rect1Line1 = line(start = [0, 0], end = [1, 0])
14518  rect1Line2 = line(start = [1, 0], end = [1, 1])
14519  rect1Line3 = line(start = [1, 1], end = [0, 1])
14520  rect1Line4 = line(start = [0, 1], end = [0, 0])
14521  rect2Line1 = line(start = [3, 0], end = [4, 0])
14522  rect2Line2 = line(start = [4, 0], end = [4, 1])
14523  rect2Line3 = line(start = [4, 1], end = [3, 1])
14524  rect2Line4 = line(start = [3, 1], end = [3, 0])
14525}
14526hidden001 = hide(sketch001)
14527region001 = region(segments = [
14528  sketch001.rect1Line4,
14529  sketch001.rect1Line1
14530])
14531region002 = region(segments = [
14532  sketch001.rect2Line4,
14533  sketch001.rect2Line1
14534])
14535extrude001 = extrude([region001, region002], length = 5)
14536";
14537
14538        let program = Program::parse(initial_source).unwrap().0.unwrap();
14539        let mut frontend = FrontendState::new();
14540        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14541        let version = Version(0);
14542
14543        frontend.hack_set_program(&ctx, program).await.unwrap();
14544        let region_call = "\
14545region(segments = [
14546  sketch001.rect1Line4,
14547  sketch001.rect1Line1
14548])";
14549        let region_call_start = initial_source.find(region_call).unwrap();
14550        let region_range = [region_call_start, region_call_start + region_call.len(), 0].into();
14551        let segment_call = "line(start = [0, 0], end = [1, 0])";
14552        let segment_call_start = initial_source.find(segment_call).unwrap();
14553        let segment_range = [segment_call_start, segment_call_start + segment_call.len(), 0].into();
14554        let wall_object_id = frontend
14555            .scene_graph
14556            .objects
14557            .iter()
14558            .find_map(|object| match &object.kind {
14559                ObjectKind::Wall(wall)
14560                    if wall.source.path.as_ref().is_some_and(|path| path.range == region_range)
14561                        && wall.source.segment.range == segment_range =>
14562                {
14563                    Some(object.id)
14564                }
14565                _ => None,
14566            })
14567            .expect("expected a wall object for region001.tags.rect1Line1");
14568
14569        let sketch_args = SketchCtor {
14570            on: Plane::Object(wall_object_id),
14571        };
14572        let (src_delta, _scene_delta, _sketch_id) = frontend
14573            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14574            .await
14575            .unwrap();
14576
14577        let expected_face = "faceOf(extrude001[0], face = region001.tags.rect1Line1)";
14578        assert!(
14579            src_delta.text.contains(expected_face),
14580            "expected `{expected_face}` in:\n{}",
14581            src_delta.text
14582        );
14583        assert!(!src_delta.text.contains("faceOf(extrude001, face ="));
14584
14585        ctx.close().await;
14586    }
14587
14588    #[test]
14589    fn test_enclosing_variable_fallback_skips_nested_sketch_items() {
14590        let source = "\
14591sketch001 = sketch(on = XY) {
14592  line(start = [0, 0], end = [1, 0])
14593}
14594part = subtract(boxSolid, tools = [cutSolid])
14595  |> appearance(color = \"#8f96a3\")
14596";
14597        let ast = Program::parse(source).unwrap().0.unwrap().ast;
14598        let line_start = source.find("line").unwrap();
14599        let line_end = line_start + "line(start = [0, 0], end = [1, 0])".len();
14600        let line_ref = SourceRef::Simple {
14601            range: [line_start, line_end, 0].into(),
14602            node_path: None,
14603        };
14604        assert_eq!(variable_name_containing_source_ref(&ast, &line_ref), None);
14605
14606        let subtract_start = source.find("subtract").unwrap();
14607        let subtract_end = subtract_start + "subtract(boxSolid, tools = [cutSolid])".len();
14608        let subtract_ref = SourceRef::Simple {
14609            range: [subtract_start, subtract_end, 0].into(),
14610            node_path: None,
14611        };
14612        assert_eq!(
14613            variable_name_containing_source_ref(&ast, &subtract_ref),
14614            Some("part".to_owned())
14615        );
14616    }
14617
14618    #[tokio::test(flavor = "multi_thread")]
14619    async fn test_sketch_on_subtracted_sweep_cap_uses_composite_solid() {
14620        clear_mem_cache().await;
14621        let source = "\
14622boxSolid = startSketchOn(XY)
14623  |> startProfile(at = [0, 0])
14624  |> line(end = [4, 0], tag = $bottomEdge)
14625  |> line(end = [0, 4])
14626  |> line(end = [-4, 0])
14627  |> close()
14628  |> extrude(length = 10)
14629cutSolid = startSketchOn(XY)
14630  |> startProfile(at = [1, 1])
14631  |> line(end = [1, 0])
14632  |> line(end = [0, 1])
14633  |> line(end = [-1, 0])
14634  |> close()
14635  |> extrude(length = 10)
14636part = subtract(boxSolid, tools = [cutSolid])
14637  |> appearance(color = \"#8f96a3\", roughness = 55, metalness = 8)
14638";
14639        let program = Program::parse(source).unwrap().0.unwrap();
14640        let mut frontend = FrontendState::new();
14641        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14642        match frontend.hack_set_program(&ctx, program).await.unwrap() {
14643            SetProgramOutcome::Success { .. } => {}
14644            SetProgramOutcome::ExecFailure { error } => panic!("KCL fixture failed to execute: {error:?}"),
14645        }
14646
14647        let sweep_call_start = source.find("extrude").unwrap();
14648        let sweep_call_end = sweep_call_start + "extrude(length = 10)".len();
14649        let part_call_start = source.find("subtract").unwrap();
14650        let part_call_end = part_call_start + "subtract(boxSolid, tools = [cutSolid])".len();
14651        let sweep_range = [sweep_call_start, sweep_call_end, 0].into();
14652        let composite_range = [part_call_start, part_call_end, 0].into();
14653
14654        let cap_object = frontend
14655            .scene_graph
14656            .objects
14657            .iter()
14658            .find(|object| {
14659                matches!(
14660                    &object.kind,
14661                    ObjectKind::Cap(crate::frontend::api::Cap {
14662                        kind: crate::frontend::api::CapKind::End,
14663                        source,
14664                        ..
14665                    }) if source.solid.range == composite_range && source.sweep.range == sweep_range
14666                )
14667            })
14668            .expect("expected end cap object to trace through subtract and original extrude");
14669
14670        let mut ast = frontend.program.ast.clone();
14671        let cap_expr = sketch_on_ast_expr(
14672            &mut ast,
14673            &frontend.scene_graph,
14674            &frontend.solid_references,
14675            &Plane::Object(cap_object.id),
14676        )
14677        .unwrap();
14678        let cap_face_decl = ast::VariableDeclaration::new(
14679            ast::VariableDeclarator::new("capFace", cap_expr.clone()),
14680            ast::ItemVisibility::Default,
14681            ast::VariableKind::Const,
14682        );
14683        ast.body
14684            .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
14685                cap_face_decl,
14686            ))));
14687        let generated_source = source_from_ast(&ast);
14688
14689        assert!(generated_source.contains("capFace = faceOf(part, face = END)"));
14690        assert!(!generated_source.contains("faceOf(boxSolid"));
14691        let ast::Expr::CallExpressionKw(call) = cap_expr else {
14692            panic!("expected faceOf call");
14693        };
14694        assert_eq!(call.callee.name.name, "faceOf");
14695        let ast::Expr::Name(solid_name) = call.unlabeled.as_ref().unwrap() else {
14696            panic!("expected solid name");
14697        };
14698        assert_eq!(solid_name.name.name, "part");
14699        let ast::Expr::Name(face_name) = &call.arguments[0].arg else {
14700            panic!("expected face name");
14701        };
14702        assert_eq!(face_name.name.name, "END");
14703
14704        ctx.close().await;
14705    }
14706
14707    #[tokio::test(flavor = "multi_thread")]
14708    async fn test_sketch_on_plane_incremental() {
14709        let initial_source = "\
14710len = 2mm
14711cube = startSketchOn(XY)
14712  |> startProfile(at = [0, 0])
14713  |> line(end = [len, 0], tag = $side)
14714  |> line(end = [0, len])
14715  |> line(end = [-len, 0])
14716  |> line(end = [0, -len])
14717  |> close()
14718  |> extrude(length = len)
14719
14720plane = planeOf(cube, face = side)
14721";
14722
14723        let program = Program::parse(initial_source).unwrap().0.unwrap();
14724
14725        let mut frontend = FrontendState::new();
14726
14727        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14728        let mock_ctx = ExecutorContext::new_mock(None).await;
14729        let version = Version(0);
14730
14731        frontend.hack_set_program(&ctx, program).await.unwrap();
14732        // Find the last plane since the first plane is the XY plane.
14733        let plane_object = frontend
14734            .scene_graph
14735            .objects
14736            .iter()
14737            .rev()
14738            .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
14739            .unwrap();
14740        let plane_id = plane_object.id;
14741
14742        let sketch_args = SketchCtor {
14743            on: Plane::Object(plane_id),
14744        };
14745        let (src_delta, scene_delta, sketch_id) = frontend
14746            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14747            .await
14748            .unwrap();
14749        insta::assert_snapshot!("test_sketch_on_plane_incremental", src_delta.text.as_str());
14750        assert_eq!(sketch_id, ObjectId(2));
14751        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14752        let sketch_object = &scene_delta.new_graph.objects[2];
14753        assert_eq!(sketch_object.id, ObjectId(2));
14754        assert_eq!(
14755            sketch_object.kind,
14756            ObjectKind::Sketch(Sketch {
14757                args: SketchCtor {
14758                    on: Plane::Object(plane_id),
14759                },
14760                plane: plane_id,
14761                segments: vec![],
14762                constraints: vec![],
14763            })
14764        );
14765        assert_eq!(scene_delta.new_graph.objects.len(), 9);
14766
14767        let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
14768        assert_eq!(plane_object.id, plane_id);
14769        assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
14770
14771        ctx.close().await;
14772        mock_ctx.close().await;
14773    }
14774
14775    #[tokio::test(flavor = "multi_thread")]
14776    async fn test_new_sketch_uses_unique_variable_name() {
14777        let initial_source = "\
14778sketch1 = sketch(on = XY) {
14779}
14780";
14781
14782        let program = Program::parse(initial_source).unwrap().0.unwrap();
14783
14784        let mut frontend = FrontendState::new();
14785        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14786        let version = Version(0);
14787
14788        frontend.hack_set_program(&ctx, program).await.unwrap();
14789
14790        let sketch_args = SketchCtor {
14791            on: Plane::Default(PlaneName::Yz),
14792        };
14793        let (src_delta, _, _) = frontend
14794            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14795            .await
14796            .unwrap();
14797
14798        insta::assert_snapshot!("test_new_sketch_uses_unique_variable_name", src_delta.text.as_str());
14799
14800        ctx.close().await;
14801    }
14802
14803    #[tokio::test(flavor = "multi_thread")]
14804    async fn test_new_sketch_twice_using_same_plane() {
14805        let initial_source = "\
14806sketch1 = sketch(on = XY) {
14807}
14808";
14809
14810        let program = Program::parse(initial_source).unwrap().0.unwrap();
14811
14812        let mut frontend = FrontendState::new();
14813        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14814        let version = Version(0);
14815
14816        frontend.hack_set_program(&ctx, program).await.unwrap();
14817
14818        let sketch_args = SketchCtor {
14819            on: Plane::Default(PlaneName::Xy),
14820        };
14821        let (src_delta, _, _) = frontend
14822            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14823            .await
14824            .unwrap();
14825
14826        insta::assert_snapshot!("test_new_sketch_twice_using_same_plane", src_delta.text.as_str());
14827
14828        ctx.close().await;
14829    }
14830
14831    #[tokio::test(flavor = "multi_thread")]
14832    async fn test_sketch_mode_reuses_cached_on_expression() {
14833        let initial_source = "\
14834width = 2mm
14835sketch(on = offsetPlane(XY, offset = width)) {
14836  line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
14837  distance([line1.start, line1.end]) == width
14838}
14839";
14840        let program = Program::parse(initial_source).unwrap().0.unwrap();
14841
14842        let mut frontend = FrontendState::new();
14843        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14844        let mock_ctx = ExecutorContext::new_mock(None).await;
14845        let version = Version(0);
14846        let project_id = ProjectId(0);
14847        let file_id = FileId(0);
14848
14849        frontend.hack_set_program(&ctx, program).await.unwrap();
14850        let initial_object_count = frontend.scene_graph.objects.len();
14851        let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14852            .expect("Expected sketch object to exist")
14853            .id;
14854
14855        // Entering sketch mode should reuse cached `on` expression state
14856        // (offsetPlane result), not fail or create extra on-surface objects.
14857        let scene_delta = frontend
14858            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14859            .await
14860            .unwrap();
14861        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14862
14863        // A follow-up sketch-mode execution should keep the same stable object
14864        // graph shape as well.
14865        let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
14866        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14867
14868        ctx.close().await;
14869        mock_ctx.close().await;
14870    }
14871
14872    #[tokio::test(flavor = "multi_thread")]
14873    async fn test_edit_sketch_nested_in_pipe() {
14874        clear_mem_cache().await;
14875        let source = r#"
14876profile = sketch(on = XY) {
14877  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
14878}
14879  |> translate(x = 2mm)
14880"#;
14881        let program = Program::parse_no_errs(source).unwrap();
14882        let mut frontend = FrontendState::new();
14883        let mock_ctx = ExecutorContext::new_mock(None).await;
14884        let version = Version(0);
14885
14886        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
14887        let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14888            .expect("Expected piped sketch object")
14889            .id;
14890
14891        let scene_delta = frontend
14892            .edit_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_id)
14893            .await
14894            .unwrap();
14895        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch_id));
14896        assert!(
14897            scene_delta
14898                .new_graph
14899                .objects
14900                .iter()
14901                .any(|object| matches!(&object.kind, ObjectKind::Segment { .. })),
14902            "Expected the piped sketch's segments to be present in sketch mode"
14903        );
14904
14905        clear_mem_cache().await;
14906        mock_ctx.close().await;
14907    }
14908
14909    #[tokio::test(flavor = "multi_thread")]
14910    async fn test_issue_9409_edit_sketch_nested_in_if_with_var_feedback() {
14911        clear_mem_cache().await;
14912        let source = r#"
14913useFirstProfile = true
14914
14915profile = if useFirstProfile {
14916  sketch(on = XY) {
14917    line1 = line(start = [0mm, 0mm], end = [var 20mm, var 10mm])
14918  }
14919} else {
14920  sketch(on = XY) {
14921    line2 = line(start = [0mm, 0mm], end = [var 10mm, var 20mm])
14922  }
14923}
14924"#;
14925        let program = Program::parse_no_errs(source).unwrap();
14926        let mut frontend = FrontendState::new();
14927        let mock_ctx = ExecutorContext::new_mock(None).await;
14928        let version = Version(0);
14929
14930        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
14931        let sketch_object =
14932            find_first_sketch_object(&frontend.scene_graph).expect("Expected active branch's sketch object");
14933        let sketch_id = sketch_object.id;
14934        let sketch = expect_sketch(sketch_object);
14935        let line_end_id = *sketch
14936            .segments
14937            .get(1)
14938            .expect("Expected the active branch's line end point");
14939
14940        let scene_delta = frontend
14941            .edit_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_id)
14942            .await
14943            .unwrap();
14944        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch_id));
14945
14946        let segments = vec![ExistingSegmentCtor {
14947            id: line_end_id,
14948            ctor: SegmentCtor::Point(PointCtor {
14949                position: Point2d {
14950                    x: Expr::Var(Number {
14951                        value: 30.0,
14952                        units: NumericSuffix::Mm,
14953                    }),
14954                    y: Expr::Var(Number {
14955                        value: 15.0,
14956                        units: NumericSuffix::Mm,
14957                    }),
14958                },
14959            }),
14960        }];
14961        let (source_delta, _) = frontend
14962            .edit_segments(&mock_ctx, version, sketch_id, segments)
14963            .await
14964            .unwrap();
14965        assert!(
14966            source_delta
14967                .text
14968                .contains("line1 = line(start = [0mm, 0mm], end = [var 30mm, var 15mm])"),
14969            "Expected the active branch's dragged variables to be updated:\n{}",
14970            source_delta.text
14971        );
14972        assert!(
14973            source_delta
14974                .text
14975                .contains("line2 = line(start = [0mm, 0mm], end = [var 10mm, var 20mm])"),
14976            "Expected the inactive branch to remain unchanged:\n{}",
14977            source_delta.text
14978        );
14979
14980        clear_mem_cache().await;
14981        mock_ctx.close().await;
14982    }
14983
14984    #[tokio::test(flavor = "multi_thread")]
14985    async fn test_multiple_sketch_blocks() {
14986        let initial_source = "\
14987// Cube that requires the engine.
14988width = 2
14989sketch001 = startSketchOn(XY)
14990profile001 = startProfile(sketch001, at = [0, 0])
14991  |> yLine(length = width, tag = $seg1)
14992  |> xLine(length = width)
14993  |> yLine(length = -width)
14994  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14995  |> close()
14996extrude001 = extrude(profile001, length = width)
14997
14998// Get a value that requires the engine.
14999x = segLen(seg1)
15000
15001// Triangle with side length 2*x.
15002sketch(on = XY) {
15003  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
15004  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
15005  coincident([line1.end, line2.start])
15006  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
15007  coincident([line2.end, line3.start])
15008  coincident([line3.end, line1.start])
15009  equalLength([line3, line1])
15010  equalLength([line1, line2])
15011  distance([line1.start, line1.end]) == 2*x
15012}
15013
15014// Line segment with length x.
15015sketch2 = sketch(on = XY) {
15016  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
15017  distance([line1.start, line1.end]) == x
15018}
15019";
15020
15021        let program = Program::parse(initial_source).unwrap().0.unwrap();
15022
15023        let mut frontend = FrontendState::new();
15024
15025        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15026        let mock_ctx = ExecutorContext::new_mock(None).await;
15027        let version = Version(0);
15028        let project_id = ProjectId(0);
15029        let file_id = FileId(0);
15030
15031        frontend.hack_set_program(&ctx, program).await.unwrap();
15032        let sketch_objects = frontend
15033            .scene_graph
15034            .objects
15035            .iter()
15036            .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
15037            .collect::<Vec<_>>();
15038        let sketch1_id = sketch_objects.first().unwrap().id;
15039        let sketch2_id = sketch_objects.get(1).unwrap().id;
15040        // First point in sketch1.
15041        let point1_id = ObjectId(sketch1_id.0 + 1);
15042        // First point in sketch2.
15043        let point2_id = ObjectId(sketch2_id.0 + 1);
15044
15045        // Edit the first sketch. Objects before the sketch block should be
15046        // present from execution cache so that we can sketch on prior planes,
15047        // for example. Objects after the first sketch block should not be
15048        // present since those statements are skipped in sketch mode.
15049        //
15050        // - startSketchOn(XY) Plane 1
15051        // - sketch on=XY Plane 1
15052        // - Sketch block 16
15053        let scene_delta = frontend
15054            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
15055            .await
15056            .unwrap();
15057        assert_eq!(
15058            scene_delta.new_graph.objects.len(),
15059            18,
15060            "{:#?}",
15061            scene_delta.new_graph.objects
15062        );
15063
15064        // Edit a point in the first sketch.
15065        let point_ctor = PointCtor {
15066            position: Point2d {
15067                x: Expr::Var(Number {
15068                    value: 1.0,
15069                    units: NumericSuffix::Mm,
15070                }),
15071                y: Expr::Var(Number {
15072                    value: 2.0,
15073                    units: NumericSuffix::Mm,
15074                }),
15075            },
15076        };
15077        let segments = vec![ExistingSegmentCtor {
15078            id: point1_id,
15079            ctor: SegmentCtor::Point(point_ctor),
15080        }];
15081        let (src_delta, _) = frontend
15082            .edit_segments(&mock_ctx, version, sketch1_id, segments)
15083            .await
15084            .unwrap();
15085        // Only the first sketch block changes.
15086        insta::assert_snapshot!("test_multiple_sketch_blocks_1", src_delta.text.as_str());
15087        let edited_sketch1_source = src_delta.text.clone();
15088
15089        // Execute mock to simulate drag end.
15090        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
15091        assert_eq!(src_delta.text, edited_sketch1_source);
15092        // Exit sketch. Objects from the entire program should be present.
15093        //
15094        // - startSketchOn(XY) Plane 1
15095        // - sketch on=XY Plane 1
15096        // - Sketch block 16
15097        // - sketch on=XY cached
15098        // - Sketch block 5
15099        let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
15100        assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
15101
15102        // Edit the second sketch.
15103        //
15104        // - startSketchOn(XY) Plane 1
15105        // - sketch on=XY Plane 1
15106        // - Sketch block 16
15107        // - sketch on=XY cached
15108        // - Sketch block 5
15109        let scene_delta = frontend
15110            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
15111            .await
15112            .unwrap();
15113        assert_eq!(
15114            scene_delta.new_graph.objects.len(),
15115            24,
15116            "{:#?}",
15117            scene_delta.new_graph.objects
15118        );
15119
15120        // Edit a point in the second sketch.
15121        let point_ctor = PointCtor {
15122            position: Point2d {
15123                x: Expr::Var(Number {
15124                    value: 3.0,
15125                    units: NumericSuffix::Mm,
15126                }),
15127                y: Expr::Var(Number {
15128                    value: 4.0,
15129                    units: NumericSuffix::Mm,
15130                }),
15131            },
15132        };
15133        let segments = vec![ExistingSegmentCtor {
15134            id: point2_id,
15135            ctor: SegmentCtor::Point(point_ctor),
15136        }];
15137        let (src_delta, _) = frontend
15138            .edit_segments(&mock_ctx, version, sketch2_id, segments)
15139            .await
15140            .unwrap();
15141        // Only the second sketch block changes.
15142        insta::assert_snapshot!("test_multiple_sketch_blocks_2", src_delta.text.as_str());
15143        let edited_sketch2_source = src_delta.text.clone();
15144
15145        // Execute mock to simulate drag end.
15146        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
15147        assert_eq!(src_delta.text, edited_sketch2_source);
15148
15149        ctx.close().await;
15150        mock_ctx.close().await;
15151    }
15152
15153    #[tokio::test(flavor = "multi_thread")]
15154    async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
15155        clear_mem_cache().await;
15156
15157        let source = r#"sketch001 = sketch(on = XZ) {
15158  circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
15159}
15160sketch002 = sketch(on = XY) {
15161  line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
15162  line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
15163  line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
15164  line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
15165  coincident([line1.end, line2.start])
15166  coincident([line2.end, line3.start])
15167  coincident([line3.end, line4.start])
15168  coincident([line4.end, line1.start])
15169  parallel([line2, line4])
15170  parallel([line3, line1])
15171  perpendicular([line1, line2])
15172  horizontal(line3)
15173  coincident([line1.start, ORIGIN])
15174}
15175"#;
15176
15177        let program = Program::parse(source).unwrap().0.unwrap();
15178        let mut frontend = FrontendState::new();
15179        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
15180        let mock_ctx = ExecutorContext::new_mock(None).await;
15181        let version = Version(0);
15182        let project_id = ProjectId(0);
15183        let file_id = FileId(0);
15184
15185        frontend.hack_set_program(&ctx, program).await.unwrap();
15186        let sketch_objects = frontend
15187            .scene_graph
15188            .objects
15189            .iter()
15190            .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
15191            .collect::<Vec<_>>();
15192        assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
15193
15194        let sketch1_id = sketch_objects[0].id;
15195        let sketch2_id = sketch_objects[1].id;
15196
15197        frontend
15198            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
15199            .await
15200            .unwrap();
15201        frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
15202
15203        let scene_delta = frontend
15204            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
15205            .await
15206            .unwrap();
15207        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
15208
15209        clear_mem_cache().await;
15210        ctx.close().await;
15211        mock_ctx.close().await;
15212    }
15213
15214    // Regression tests: operations on source code with extra whitespace/newlines.
15215    // These test that NodePath-based lookups work correctly when source ranges
15216    // are shifted by extra whitespace that wouldn't be present after formatting.
15217
15218    #[tokio::test(flavor = "multi_thread")]
15219    async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
15220        // Extra newlines after @settings line - this shifts all source ranges.
15221        let initial_source = "@settings(defaultLengthUnit = mm)
15222
15223sketch001 = sketch(on = XY) {
15224  point(at = [1in, 2in])
15225}
15226";
15227
15228        let program = Program::parse(initial_source).unwrap().0.unwrap();
15229        let mut frontend = FrontendState::new();
15230
15231        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15232        let mock_ctx = ExecutorContext::new_mock(None).await;
15233        let version = Version(0);
15234        let project_id = ProjectId(0);
15235        let file_id = FileId(0);
15236
15237        frontend.hack_set_program(&ctx, program).await.unwrap();
15238        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15239        let sketch_id = sketch_object.id;
15240
15241        // Edit sketch should succeed despite extra newlines.
15242        frontend
15243            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15244            .await
15245            .unwrap();
15246
15247        // Add a new point to the sketch.
15248        let point_ctor = PointCtor {
15249            position: Point2d {
15250                x: Expr::Number(Number {
15251                    value: 5.0,
15252                    units: NumericSuffix::Mm,
15253                }),
15254                y: Expr::Number(Number {
15255                    value: 6.0,
15256                    units: NumericSuffix::Mm,
15257                }),
15258            },
15259        };
15260        let segment = SegmentCtor::Point(point_ctor);
15261        let (src_delta, scene_delta) = frontend
15262            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15263            .await
15264            .unwrap();
15265        // After adding a point, the source should be reformatted with standard whitespace.
15266        assert!(
15267            src_delta.text.contains("point(at = [5mm, 6mm])"),
15268            "Expected new point in source, got: {}",
15269            src_delta.text
15270        );
15271        assert!(!scene_delta.new_objects.is_empty());
15272
15273        ctx.close().await;
15274        mock_ctx.close().await;
15275    }
15276
15277    #[tokio::test(flavor = "multi_thread")]
15278    async fn test_ensure_control_point_spline_experimental_features_adds_allow_setting() {
15279        let initial_program = Program::parse("s = sketch(on = XY) {}\n").unwrap().0.unwrap();
15280
15281        let updated_program = ensure_control_point_spline_experimental_features(&initial_program).unwrap();
15282        let meta_settings = updated_program.meta_settings().unwrap().unwrap();
15283
15284        assert_eq!(meta_settings.experimental_features, WarningLevel::Allow);
15285        assert!(
15286            source_from_ast(&updated_program.ast).contains("@settings(experimentalFeatures = allow)"),
15287            "Expected experimental settings to be added to source"
15288        );
15289    }
15290
15291    #[tokio::test(flavor = "multi_thread")]
15292    async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
15293        // Extra newlines after @settings, with an empty sketch block.
15294        let initial_source = "@settings(defaultLengthUnit = mm)
15295
15296s = sketch(on = XY) {}
15297";
15298
15299        let program = Program::parse(initial_source).unwrap().0.unwrap();
15300        let mut frontend = FrontendState::new();
15301
15302        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15303        let mock_ctx = ExecutorContext::new_mock(None).await;
15304        let version = Version(0);
15305
15306        frontend.hack_set_program(&ctx, program).await.unwrap();
15307        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15308        let sketch_id = sketch_object.id;
15309
15310        let line_ctor = LineCtor {
15311            start: Point2d {
15312                x: Expr::Number(Number {
15313                    value: 0.0,
15314                    units: NumericSuffix::Mm,
15315                }),
15316                y: Expr::Number(Number {
15317                    value: 0.0,
15318                    units: NumericSuffix::Mm,
15319                }),
15320            },
15321            end: Point2d {
15322                x: Expr::Number(Number {
15323                    value: 10.0,
15324                    units: NumericSuffix::Mm,
15325                }),
15326                y: Expr::Number(Number {
15327                    value: 10.0,
15328                    units: NumericSuffix::Mm,
15329                }),
15330            },
15331            construction: None,
15332        };
15333        let segment = SegmentCtor::Line(line_ctor);
15334        let (src_delta, scene_delta) = frontend
15335            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15336            .await
15337            .unwrap();
15338        assert!(
15339            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15340            "Expected line in source, got: {}",
15341            src_delta.text
15342        );
15343        // Line creates start point, end point, and line segment.
15344        assert_eq!(scene_delta.new_objects.len(), 3);
15345
15346        ctx.close().await;
15347        mock_ctx.close().await;
15348    }
15349
15350    #[tokio::test(flavor = "multi_thread")]
15351    async fn test_extra_newlines_between_operations_edit_line() {
15352        // Extra newlines between @settings and sketch, and inside the sketch block.
15353        let initial_source = "@settings(defaultLengthUnit = mm)
15354
15355sketch001 = sketch(on = XY) {
15356
15357  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
15358
15359}
15360";
15361
15362        let program = Program::parse(initial_source).unwrap().0.unwrap();
15363        let mut frontend = FrontendState::new();
15364
15365        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15366        let mock_ctx = ExecutorContext::new_mock(None).await;
15367        let version = Version(0);
15368        let project_id = ProjectId(0);
15369        let file_id = FileId(0);
15370
15371        frontend.hack_set_program(&ctx, program).await.unwrap();
15372        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15373        let sketch_id = sketch_object.id;
15374        let sketch = expect_sketch(sketch_object);
15375
15376        // Extract segment IDs before edit_sketch borrows frontend mutably.
15377        let line_id = sketch
15378            .segments
15379            .iter()
15380            .copied()
15381            .find(|seg_id| {
15382                matches!(
15383                    &frontend.scene_graph.objects[seg_id.0].kind,
15384                    ObjectKind::Segment {
15385                        segment: Segment::Line(_)
15386                    }
15387                )
15388            })
15389            .expect("Expected a line segment in sketch");
15390
15391        // Enter sketch edit mode.
15392        frontend
15393            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15394            .await
15395            .unwrap();
15396
15397        // Edit the line.
15398        let line_ctor = LineCtor {
15399            start: Point2d {
15400                x: Expr::Var(Number {
15401                    value: 1.0,
15402                    units: NumericSuffix::Mm,
15403                }),
15404                y: Expr::Var(Number {
15405                    value: 2.0,
15406                    units: NumericSuffix::Mm,
15407                }),
15408            },
15409            end: Point2d {
15410                x: Expr::Var(Number {
15411                    value: 13.0,
15412                    units: NumericSuffix::Mm,
15413                }),
15414                y: Expr::Var(Number {
15415                    value: 14.0,
15416                    units: NumericSuffix::Mm,
15417                }),
15418            },
15419            construction: None,
15420        };
15421        let segments = vec![ExistingSegmentCtor {
15422            id: line_id,
15423            ctor: SegmentCtor::Line(line_ctor),
15424        }];
15425        let (src_delta, _scene_delta) = frontend
15426            .edit_segments(&mock_ctx, version, sketch_id, segments)
15427            .await
15428            .unwrap();
15429        assert!(
15430            src_delta
15431                .text
15432                .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
15433            "Expected edited line in source, got: {}",
15434            src_delta.text
15435        );
15436
15437        ctx.close().await;
15438        mock_ctx.close().await;
15439    }
15440
15441    #[tokio::test(flavor = "multi_thread")]
15442    async fn test_extra_newlines_delete_segment() {
15443        // Extra whitespace before and after the sketch block.
15444        let initial_source = "@settings(defaultLengthUnit = mm)
15445
15446sketch001 = sketch(on = XY) {
15447  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
15448}
15449";
15450
15451        let program = Program::parse(initial_source).unwrap().0.unwrap();
15452        let mut frontend = FrontendState::new();
15453
15454        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15455        let mock_ctx = ExecutorContext::new_mock(None).await;
15456        let version = Version(0);
15457
15458        frontend.hack_set_program(&ctx, program).await.unwrap();
15459        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15460        let sketch_id = sketch_object.id;
15461        let sketch = expect_sketch(sketch_object);
15462
15463        // The sketch should have 3 segments: start point, center point, and the circle.
15464        assert_eq!(sketch.segments.len(), 3);
15465        let circle_id = sketch.segments[2];
15466
15467        // Delete the circle despite extra newlines in original source.
15468        let (src_delta, scene_delta) = frontend
15469            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
15470            .await
15471            .unwrap();
15472        assert!(
15473            src_delta.text.contains("sketch(on = XY) {"),
15474            "Expected sketch block in source, got: {}",
15475            src_delta.text
15476        );
15477        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
15478        let new_sketch = expect_sketch(new_sketch_object);
15479        assert_eq!(new_sketch.segments.len(), 0);
15480
15481        ctx.close().await;
15482        mock_ctx.close().await;
15483    }
15484
15485    #[tokio::test(flavor = "multi_thread")]
15486    async fn test_unformatted_source_add_arc() {
15487        // Source with inconsistent whitespace - tabs, extra spaces, multiple blank lines.
15488        let initial_source = "@settings(defaultLengthUnit = mm)
15489
15490sketch001 = sketch(on = XY) {
15491}
15492";
15493
15494        let program = Program::parse(initial_source).unwrap().0.unwrap();
15495        let mut frontend = FrontendState::new();
15496
15497        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15498        let mock_ctx = ExecutorContext::new_mock(None).await;
15499        let version = Version(0);
15500
15501        frontend.hack_set_program(&ctx, program).await.unwrap();
15502        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15503        let sketch_id = sketch_object.id;
15504
15505        let arc_ctor = ArcCtor {
15506            start: Point2d {
15507                x: Expr::Var(Number {
15508                    value: 5.0,
15509                    units: NumericSuffix::Mm,
15510                }),
15511                y: Expr::Var(Number {
15512                    value: 0.0,
15513                    units: NumericSuffix::Mm,
15514                }),
15515            },
15516            end: Point2d {
15517                x: Expr::Var(Number {
15518                    value: 0.0,
15519                    units: NumericSuffix::Mm,
15520                }),
15521                y: Expr::Var(Number {
15522                    value: 5.0,
15523                    units: NumericSuffix::Mm,
15524                }),
15525            },
15526            center: Point2d {
15527                x: Expr::Var(Number {
15528                    value: 0.0,
15529                    units: NumericSuffix::Mm,
15530                }),
15531                y: Expr::Var(Number {
15532                    value: 0.0,
15533                    units: NumericSuffix::Mm,
15534                }),
15535            },
15536            direction: None,
15537            construction: None,
15538        };
15539        let segment = SegmentCtor::Arc(arc_ctor);
15540        let (src_delta, scene_delta) = frontend
15541            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15542            .await
15543            .unwrap();
15544        assert!(
15545            src_delta
15546                .text
15547                .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
15548            "Expected arc in source, got: {}",
15549            src_delta.text
15550        );
15551        assert!(!scene_delta.new_objects.is_empty());
15552
15553        ctx.close().await;
15554        mock_ctx.close().await;
15555    }
15556
15557    #[tokio::test(flavor = "multi_thread")]
15558    async fn test_arc_direction_flows_to_source() {
15559        let initial_source = "@settings(defaultLengthUnit = mm)
15560
15561sketch001 = sketch(on = XY) {
15562}
15563";
15564
15565        let program = Program::parse(initial_source).unwrap().0.unwrap();
15566        let mut frontend = FrontendState::new();
15567
15568        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15569        let mock_ctx = ExecutorContext::new_mock(None).await;
15570        let version = Version(0);
15571
15572        frontend.hack_set_program(&ctx, program).await.unwrap();
15573        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15574        let sketch_id = sketch_object.id;
15575
15576        let point = |x: f64, y: f64| Point2d {
15577            x: Expr::Var(Number {
15578                value: x,
15579                units: NumericSuffix::Mm,
15580            }),
15581            y: Expr::Var(Number {
15582                value: y,
15583                units: NumericSuffix::Mm,
15584            }),
15585        };
15586
15587        // Adding a clockwise arc writes its direction to the source.
15588        let arc_ctor = ArcCtor {
15589            start: point(5.0, 0.0),
15590            end: point(0.0, 5.0),
15591            center: point(0.0, 0.0),
15592            direction: Some(ArcDirection::Cw),
15593            construction: None,
15594        };
15595        let (src_delta, scene_delta) = frontend
15596            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
15597            .await
15598            .unwrap();
15599        assert!(
15600            src_delta.text.contains("direction = CW"),
15601            "Expected direction = CW in source, got: {}",
15602            src_delta.text
15603        );
15604        // The new objects are the end points, the center, and then the arc.
15605        let arc_id = *scene_delta.new_objects.last().unwrap();
15606
15607        // Editing the arc's points preserves the clockwise direction. The
15608        // edited points keep the same distance to the center so that the
15609        // solver doesn't need to move anything.
15610        let edited_ctor = ArcCtor {
15611            start: point(0.0, -5.0),
15612            end: point(0.0, 5.0),
15613            center: point(0.0, 0.0),
15614            direction: Some(ArcDirection::Cw),
15615            construction: None,
15616        };
15617        let (src_delta, _scene_delta) = frontend
15618            .edit_segments(
15619                &mock_ctx,
15620                version,
15621                sketch_id,
15622                vec![ExistingSegmentCtor {
15623                    id: arc_id,
15624                    ctor: SegmentCtor::Arc(edited_ctor),
15625                }],
15626            )
15627            .await
15628            .unwrap();
15629        assert!(
15630            src_delta.text.contains("start = [var 0mm, var -5mm]"),
15631            "Expected edited start point in source, got: {}",
15632            src_delta.text
15633        );
15634        assert!(
15635            src_delta.text.contains("direction = CW"),
15636            "Expected direction = CW to be preserved in source, got: {}",
15637            src_delta.text
15638        );
15639
15640        // Editing the arc back to counterclockwise removes the direction
15641        // argument since counterclockwise is the default.
15642        let edited_ctor = ArcCtor {
15643            start: point(0.0, -5.0),
15644            end: point(0.0, 5.0),
15645            center: point(0.0, 0.0),
15646            direction: Some(ArcDirection::Ccw),
15647            construction: None,
15648        };
15649        let (src_delta, _scene_delta) = frontend
15650            .edit_segments(
15651                &mock_ctx,
15652                version,
15653                sketch_id,
15654                vec![ExistingSegmentCtor {
15655                    id: arc_id,
15656                    ctor: SegmentCtor::Arc(edited_ctor),
15657                }],
15658            )
15659            .await
15660            .unwrap();
15661        assert!(
15662            !src_delta.text.contains("direction"),
15663            "Expected direction argument to be removed from source, got: {}",
15664            src_delta.text
15665        );
15666
15667        ctx.close().await;
15668        mock_ctx.close().await;
15669    }
15670
15671    #[tokio::test(flavor = "multi_thread")]
15672    async fn test_extra_newlines_add_circle() {
15673        // Extra blank lines between settings and sketch.
15674        let initial_source = "@settings(defaultLengthUnit = mm)
15675
15676sketch001 = sketch(on = XY) {
15677}
15678";
15679
15680        let program = Program::parse(initial_source).unwrap().0.unwrap();
15681        let mut frontend = FrontendState::new();
15682
15683        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15684        let mock_ctx = ExecutorContext::new_mock(None).await;
15685        let version = Version(0);
15686
15687        frontend.hack_set_program(&ctx, program).await.unwrap();
15688        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15689        let sketch_id = sketch_object.id;
15690
15691        let circle_ctor = CircleCtor {
15692            start: Point2d {
15693                x: Expr::Var(Number {
15694                    value: 5.0,
15695                    units: NumericSuffix::Mm,
15696                }),
15697                y: Expr::Var(Number {
15698                    value: 0.0,
15699                    units: NumericSuffix::Mm,
15700                }),
15701            },
15702            center: Point2d {
15703                x: Expr::Var(Number {
15704                    value: 0.0,
15705                    units: NumericSuffix::Mm,
15706                }),
15707                y: Expr::Var(Number {
15708                    value: 0.0,
15709                    units: NumericSuffix::Mm,
15710                }),
15711            },
15712            construction: None,
15713        };
15714        let segment = SegmentCtor::Circle(circle_ctor);
15715        let (src_delta, scene_delta) = frontend
15716            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15717            .await
15718            .unwrap();
15719        assert!(
15720            src_delta
15721                .text
15722                .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
15723            "Expected circle in source, got: {}",
15724            src_delta.text
15725        );
15726        assert!(!scene_delta.new_objects.is_empty());
15727
15728        ctx.close().await;
15729        mock_ctx.close().await;
15730    }
15731
15732    #[tokio::test(flavor = "multi_thread")]
15733    async fn test_extra_newlines_add_constraint() {
15734        // Extra newlines with a sketch containing two lines - add a coincident constraint.
15735        let initial_source = "@settings(defaultLengthUnit = mm)
15736
15737sketch001 = sketch(on = XY) {
15738  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
15739  line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
15740}
15741";
15742
15743        let program = Program::parse(initial_source).unwrap().0.unwrap();
15744        let mut frontend = FrontendState::new();
15745
15746        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15747        let mock_ctx = ExecutorContext::new_mock(None).await;
15748        let version = Version(0);
15749        let project_id = ProjectId(0);
15750        let file_id = FileId(0);
15751
15752        frontend.hack_set_program(&ctx, program).await.unwrap();
15753        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15754        let sketch_id = sketch_object.id;
15755        let sketch = expect_sketch(sketch_object);
15756
15757        // Extract segment data before edit_sketch borrows frontend mutably.
15758        let line_ids: Vec<ObjectId> = sketch
15759            .segments
15760            .iter()
15761            .copied()
15762            .filter(|seg_id| {
15763                matches!(
15764                    &frontend.scene_graph.objects[seg_id.0].kind,
15765                    ObjectKind::Segment {
15766                        segment: Segment::Line(_)
15767                    }
15768                )
15769            })
15770            .collect();
15771        assert_eq!(line_ids.len(), 2, "Expected two line segments");
15772
15773        let line1 = &frontend.scene_graph.objects[line_ids[0].0];
15774        let ObjectKind::Segment {
15775            segment: Segment::Line(line1_data),
15776        } = &line1.kind
15777        else {
15778            panic!("Expected line");
15779        };
15780        let line2 = &frontend.scene_graph.objects[line_ids[1].0];
15781        let ObjectKind::Segment {
15782            segment: Segment::Line(line2_data),
15783        } = &line2.kind
15784        else {
15785            panic!("Expected line");
15786        };
15787
15788        // Build constraint before entering sketch mode.
15789        let constraint = Constraint::Coincident(Coincident {
15790            segments: vec![line1_data.end.into(), line2_data.start.into()],
15791        });
15792
15793        // Enter sketch edit mode.
15794        frontend
15795            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15796            .await
15797            .unwrap();
15798        let (src_delta, _scene_delta) = frontend
15799            .add_constraint(&mock_ctx, version, sketch_id, constraint)
15800            .await
15801            .unwrap();
15802        assert!(
15803            src_delta.text.contains("coincident("),
15804            "Expected coincident constraint in source, got: {}",
15805            src_delta.text
15806        );
15807
15808        ctx.close().await;
15809        mock_ctx.close().await;
15810    }
15811
15812    #[tokio::test(flavor = "multi_thread")]
15813    async fn test_extra_newlines_add_line_then_edit_line() {
15814        // Extra newlines after @settings - add a line, then edit it.
15815        let initial_source = "@settings(defaultLengthUnit = mm)
15816
15817sketch001 = sketch(on = XY) {
15818}
15819";
15820
15821        let program = Program::parse(initial_source).unwrap().0.unwrap();
15822        let mut frontend = FrontendState::new();
15823
15824        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15825        let mock_ctx = ExecutorContext::new_mock(None).await;
15826        let version = Version(0);
15827
15828        frontend.hack_set_program(&ctx, program).await.unwrap();
15829        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15830        let sketch_id = sketch_object.id;
15831
15832        // Add a line.
15833        let line_ctor = LineCtor {
15834            start: Point2d {
15835                x: Expr::Number(Number {
15836                    value: 0.0,
15837                    units: NumericSuffix::Mm,
15838                }),
15839                y: Expr::Number(Number {
15840                    value: 0.0,
15841                    units: NumericSuffix::Mm,
15842                }),
15843            },
15844            end: Point2d {
15845                x: Expr::Number(Number {
15846                    value: 10.0,
15847                    units: NumericSuffix::Mm,
15848                }),
15849                y: Expr::Number(Number {
15850                    value: 10.0,
15851                    units: NumericSuffix::Mm,
15852                }),
15853            },
15854            construction: None,
15855        };
15856        let segment = SegmentCtor::Line(line_ctor);
15857        let (src_delta, scene_delta) = frontend
15858            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15859            .await
15860            .unwrap();
15861        assert!(
15862            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15863            "Expected line in source after add, got: {}",
15864            src_delta.text
15865        );
15866        // Line creates start point, end point, and line segment.
15867        let line_id = *scene_delta.new_objects.last().unwrap();
15868
15869        // Edit the line.
15870        let line_ctor = LineCtor {
15871            start: Point2d {
15872                x: Expr::Number(Number {
15873                    value: 1.0,
15874                    units: NumericSuffix::Mm,
15875                }),
15876                y: Expr::Number(Number {
15877                    value: 2.0,
15878                    units: NumericSuffix::Mm,
15879                }),
15880            },
15881            end: Point2d {
15882                x: Expr::Number(Number {
15883                    value: 13.0,
15884                    units: NumericSuffix::Mm,
15885                }),
15886                y: Expr::Number(Number {
15887                    value: 14.0,
15888                    units: NumericSuffix::Mm,
15889                }),
15890            },
15891            construction: None,
15892        };
15893        let segments = vec![ExistingSegmentCtor {
15894            id: line_id,
15895            ctor: SegmentCtor::Line(line_ctor),
15896        }];
15897        let (src_delta, scene_delta) = frontend
15898            .edit_segments(&mock_ctx, version, sketch_id, segments)
15899            .await
15900            .unwrap();
15901        assert!(
15902            src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
15903            "Expected edited line in source, got: {}",
15904            src_delta.text
15905        );
15906        assert_eq!(scene_delta.new_objects, vec![]);
15907
15908        ctx.close().await;
15909        mock_ctx.close().await;
15910    }
15911
15912    #[test]
15913    fn test_add_variable_declaration_uses_top_level_scope_after_sketch_block() {
15914        // A non-target sketch block appears before the target so that the
15915        // traversal enters and leaves it before reaching the target. The
15916        // generated name must come from the top-level scope, where foo1 is
15917        // taken, not the sketch's scope, where no foo names are taken. This
15918        // is a regression test: dfs_mut used to visit the sketch block twice,
15919        // pushing its scope twice but popping it once, leaving the sketch
15920        // scope on top of the defined-names stack for the rest of the
15921        // traversal.
15922        let code = "\
15923foo1 = 1
15924sk = sketch() {
15925  p = var 1.5
15926}
159277 + 8
15928";
15929        let mut ast = crate::parsing::top_level_parse(code).unwrap();
15930        let ast::BodyItem::ExpressionStatement(stmt) = &ast.body[2] else {
15931            panic!("expected an expression statement");
15932        };
15933        let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
15934        let (_, cmd_return) = mutate_ast_node_by_source_ref(
15935            &mut ast,
15936            &source_ref,
15937            AstMutateCommand::AddVariableDeclaration {
15938                prefix: "foo".to_owned(),
15939            },
15940        )
15941        .unwrap();
15942        let AstMutateCommandReturn::Name(name) = cmd_return else {
15943            panic!("expected a generated name");
15944        };
15945        assert_eq!(name, "foo2");
15946        let ast::BodyItem::VariableDeclaration(decl) = &ast.body[2] else {
15947            panic!("expected the expression statement to become a variable declaration");
15948        };
15949        assert_eq!(decl.name(), "foo2");
15950    }
15951
15952    /// Get the function body of the variable declaration at `ast.body[index]`.
15953    fn function_body_at(ast: &ast::Node<ast::Program>, index: usize) -> &ast::Node<ast::Program> {
15954        let ast::BodyItem::VariableDeclaration(decl) = &ast.body[index] else {
15955            panic!("expected a variable declaration");
15956        };
15957        let ast::Expr::FunctionExpression(func) = &decl.declaration.init else {
15958            panic!("expected a function expression");
15959        };
15960        &func.body
15961    }
15962
15963    /// Get the then-branch block of the if-expression initializing the
15964    /// variable declaration at `ast.body[index]`.
15965    fn then_block_at(ast: &ast::Node<ast::Program>, index: usize) -> &ast::Node<ast::Program> {
15966        let ast::BodyItem::VariableDeclaration(decl) = &ast.body[index] else {
15967            panic!("expected a variable declaration");
15968        };
15969        let ast::Expr::IfExpression(if_expr) = &decl.declaration.init else {
15970            panic!("expected an if expression");
15971        };
15972        &if_expr.then_val
15973    }
15974
15975    #[test]
15976    fn test_add_variable_declaration_in_function_body_uses_function_scope() {
15977        // The generated name must come from the function body's scope, where
15978        // thing1 is taken. Before dfs_mut visited function bodies as program
15979        // nodes, the top-level scope was used instead, generating thing1 and
15980        // colliding with the local.
15981        let code = "\
15982fn build() {
15983  thing1 = 1
15984  10 + 20
15985  return thing1
15986}
15987";
15988        let mut ast = crate::parsing::top_level_parse(code).unwrap();
15989        let ast::BodyItem::ExpressionStatement(stmt) = &function_body_at(&ast, 0).body[1] else {
15990            panic!("expected an expression statement");
15991        };
15992        let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
15993        let (_, cmd_return) = mutate_ast_node_by_source_ref(
15994            &mut ast,
15995            &source_ref,
15996            AstMutateCommand::AddVariableDeclaration {
15997                prefix: "thing".to_owned(),
15998            },
15999        )
16000        .unwrap();
16001        let AstMutateCommandReturn::Name(name) = cmd_return else {
16002            panic!("expected a generated name");
16003        };
16004        assert_eq!(name, "thing2");
16005        let body = &function_body_at(&ast, 0).body;
16006        assert_eq!(body.len(), 3);
16007        let ast::BodyItem::VariableDeclaration(decl) = &body[1] else {
16008            panic!("expected the expression statement to become a variable declaration");
16009        };
16010        assert_eq!(decl.name(), "thing2");
16011        // Siblings are untouched.
16012        let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16013            panic!("expected a variable declaration");
16014        };
16015        assert_eq!(first.name(), "thing1");
16016        assert!(matches!(&body[2], ast::BodyItem::ReturnStatement(_)));
16017    }
16018
16019    #[test]
16020    fn test_delete_node_in_function_body_preserves_leading_comment() {
16021        // Before the shared body traversal, MutateBodyItem::Delete was
16022        // silently dropped inside function bodies, so this reported success
16023        // without deleting anything.
16024        let code = "\
16025fn build() {
16026  a = 1
16027  // keep me
16028  b = 2
16029  return a
16030}
16031";
16032        let mut ast = crate::parsing::top_level_parse(code).unwrap();
16033        let ast::BodyItem::VariableDeclaration(b_decl) = &function_body_at(&ast, 0).body[1] else {
16034            panic!("expected a variable declaration");
16035        };
16036        assert_eq!(b_decl.name(), "b");
16037        let source_ref = SourceRef::new(SourceRange::from(&b_decl.declaration.init), None);
16038        mutate_ast_node_by_source_ref(&mut ast, &source_ref, AstMutateCommand::DeleteNode).unwrap();
16039        let body = &function_body_at(&ast, 0).body;
16040        assert_eq!(body.len(), 2, "expected b to be deleted");
16041        let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16042            panic!("expected a variable declaration");
16043        };
16044        assert_eq!(first.name(), "a");
16045        let ast::BodyItem::ReturnStatement(_) = &body[1] else {
16046            panic!("expected the return statement to remain");
16047        };
16048        assert!(
16049            body[1].get_comments().iter().any(|c| c.contains("keep me")),
16050            "expected the deleted item's leading comment to migrate to the next item, got: {:?}",
16051            body[1].get_comments()
16052        );
16053    }
16054
16055    #[test]
16056    fn test_add_variable_declaration_in_function_body_ignores_parameters() {
16057        // Locals in the function body are avoided: thing1 is taken, so the
16058        // generated name is thing2. But find_defined_names only sees the
16059        // block's body items, not the function's parameters, so the generated
16060        // name collides with the thing2 parameter. This pins the current
16061        // behavior.
16062        // TODO: Should function parameters be included in the scope used for
16063        // name generation?
16064        let code = "\
16065fn build(thing2) {
16066  thing1 = 1
16067  10 + 20
16068  return thing1 + thing2
16069}
16070";
16071        let mut ast = crate::parsing::top_level_parse(code).unwrap();
16072        let ast::BodyItem::ExpressionStatement(stmt) = &function_body_at(&ast, 0).body[1] else {
16073            panic!("expected an expression statement");
16074        };
16075        let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
16076        let (_, cmd_return) = mutate_ast_node_by_source_ref(
16077            &mut ast,
16078            &source_ref,
16079            AstMutateCommand::AddVariableDeclaration {
16080                prefix: "thing".to_owned(),
16081            },
16082        )
16083        .unwrap();
16084        let AstMutateCommandReturn::Name(name) = cmd_return else {
16085            panic!("expected a generated name");
16086        };
16087        assert_eq!(name, "thing2", "locals are avoided, but parameters are not");
16088    }
16089
16090    #[test]
16091    fn test_add_variable_declaration_in_if_branch_uses_branch_scope() {
16092        let code = "\
16093x = 1
16094y = if x > 0 {
16095  q1 = 1
16096  foo(q1)
16097  q1
16098} else {
16099  2
16100}
16101";
16102        let mut ast = crate::parsing::top_level_parse(code).unwrap();
16103        let ast::BodyItem::ExpressionStatement(stmt) = &then_block_at(&ast, 1).body[1] else {
16104            panic!("expected an expression statement");
16105        };
16106        let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
16107        let (_, cmd_return) = mutate_ast_node_by_source_ref(
16108            &mut ast,
16109            &source_ref,
16110            AstMutateCommand::AddVariableDeclaration { prefix: "q".to_owned() },
16111        )
16112        .unwrap();
16113        let AstMutateCommandReturn::Name(name) = cmd_return else {
16114            panic!("expected a generated name");
16115        };
16116        assert_eq!(name, "q2");
16117        let body = &then_block_at(&ast, 1).body;
16118        assert_eq!(body.len(), 3);
16119        let ast::BodyItem::VariableDeclaration(decl) = &body[1] else {
16120            panic!("expected the expression statement to become a variable declaration");
16121        };
16122        assert_eq!(decl.name(), "q2");
16123        // Siblings are untouched.
16124        let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16125            panic!("expected a variable declaration");
16126        };
16127        assert_eq!(first.name(), "q1");
16128        assert!(matches!(&body[2], ast::BodyItem::ExpressionStatement(_)));
16129    }
16130
16131    #[test]
16132    fn test_delete_node_in_if_branch_preserves_leading_comment() {
16133        // Before the shared body traversal, MutateBodyItem::Delete was
16134        // silently dropped inside if-expression branch blocks.
16135        let code = "\
16136y = if true {
16137  a = 1
16138  // keep me
16139  b = 2
16140  a + b
16141} else {
16142  2
16143}
16144";
16145        let mut ast = crate::parsing::top_level_parse(code).unwrap();
16146        let ast::BodyItem::VariableDeclaration(b_decl) = &then_block_at(&ast, 0).body[1] else {
16147            panic!("expected a variable declaration");
16148        };
16149        assert_eq!(b_decl.name(), "b");
16150        let source_ref = SourceRef::new(SourceRange::from(&b_decl.declaration.init), None);
16151        mutate_ast_node_by_source_ref(&mut ast, &source_ref, AstMutateCommand::DeleteNode).unwrap();
16152        let body = &then_block_at(&ast, 0).body;
16153        assert_eq!(body.len(), 2, "expected b to be deleted");
16154        let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16155            panic!("expected a variable declaration");
16156        };
16157        assert_eq!(first.name(), "a");
16158        let ast::BodyItem::ExpressionStatement(_) = &body[1] else {
16159            panic!("expected the tail expression to remain");
16160        };
16161        assert!(
16162            body[1].get_comments().iter().any(|c| c.contains("keep me")),
16163            "expected the deleted item's leading comment to migrate to the next item, got: {:?}",
16164            body[1].get_comments()
16165        );
16166    }
16167}