Skip to main content

kcl_lib/
frontend.rs

1use std::cell::Cell;
2use std::collections::HashMap;
3use std::collections::HashSet;
4use std::collections::VecDeque;
5use std::ops::ControlFlow;
6
7use indexmap::IndexMap;
8use kcl_api::UnitLength;
9use kcl_error::CompilationIssue;
10use kcl_error::SourceRange;
11use serde::Serialize;
12use uuid::Uuid;
13
14use crate::ExecOutcome;
15use crate::ExecutorContext;
16use crate::KclError;
17use crate::KclErrorWithOutputs;
18use crate::KclValueView;
19use crate::Program;
20use crate::SegmentDragAnchor;
21use crate::collections::AhashIndexSet;
22use crate::execution::Artifact;
23use crate::execution::ArtifactGraph;
24use crate::execution::ArtifactId;
25use crate::execution::CapSubType;
26use crate::execution::CodeRef;
27use crate::execution::MockConfig;
28use crate::execution::SKETCH_BLOCK_PARAM_ON;
29use crate::execution::annotations::WarningLevel;
30use crate::execution::cache::SketchModeState;
31use crate::execution::cache::clear_mem_cache;
32use crate::execution::cache::read_old_memory;
33use crate::execution::cache::write_old_memory;
34use crate::execution::types::adjust_length;
35use crate::fmt::format_number_literal;
36use crate::front::Angle;
37use crate::front::ArcCtor;
38use crate::front::ArcDirection;
39use crate::front::CircleCtor;
40use crate::front::ControlPointSplineCtor;
41use crate::front::Distance;
42use crate::front::EqualRadius;
43use crate::front::Error;
44use crate::front::ExecResult;
45use crate::front::FixedPoint;
46use crate::front::Freedom;
47use crate::front::LinesEqualLength;
48use crate::front::Midpoint;
49use crate::front::Object;
50use crate::front::Parallel;
51use crate::front::Perpendicular;
52use crate::front::PointCtor;
53use crate::front::Symmetric;
54use crate::front::Tangent;
55use crate::frontend::api::CapSource;
56use crate::frontend::api::Expr;
57use crate::frontend::api::FileId;
58use crate::frontend::api::Number;
59use crate::frontend::api::ObjectId;
60use crate::frontend::api::ObjectKind;
61use crate::frontend::api::Plane;
62use crate::frontend::api::ProjectId;
63use crate::frontend::api::RestoreSketchCheckpointOutcome;
64use crate::frontend::api::SceneGraph;
65use crate::frontend::api::SceneGraphDelta;
66use crate::frontend::api::SketchCheckpointId;
67use crate::frontend::api::SourceDelta;
68use crate::frontend::api::SourceRef;
69use crate::frontend::api::SourceRefRange;
70use crate::frontend::api::Version;
71use crate::frontend::api::WallSource;
72use crate::frontend::modify::find_defined_names;
73use crate::frontend::modify::next_free_name;
74use crate::frontend::modify::next_free_name_with_padding;
75use crate::frontend::sketch::Coincident;
76use crate::frontend::sketch::Constraint;
77use crate::frontend::sketch::ConstraintLabelPositionEdit;
78use crate::frontend::sketch::ConstraintSegment;
79use crate::frontend::sketch::Diameter;
80use crate::frontend::sketch::ExistingSegmentCtor;
81use crate::frontend::sketch::Horizontal;
82use crate::frontend::sketch::LineCtor;
83use crate::frontend::sketch::Point2d;
84use crate::frontend::sketch::Radius;
85use crate::frontend::sketch::Segment;
86use crate::frontend::sketch::SegmentCtor;
87use crate::frontend::sketch::SketchApi;
88use crate::frontend::sketch::SketchCtor;
89use crate::frontend::sketch::Vertical;
90use crate::id::IncIdGenerator;
91use crate::parsing::ast::types as ast;
92use crate::parsing::ast::types::BoxNode;
93use crate::parsing::ast::types::CallExpressionKw;
94use crate::parsing::ast::types::NodePathExt;
95use crate::pretty::NumericSuffix;
96use crate::std::constraints::LinesAtAngleKind;
97use crate::walk::NodeMut;
98use crate::walk::Visitable;
99use crate::walk::traverse::MutateBodyItem;
100use crate::walk::traverse::TraversalReturn;
101use crate::walk::traverse::Visitor;
102use crate::walk::traverse::dfs_mut;
103
104pub(crate) mod api;
105pub(crate) mod modify;
106pub(crate) mod sketch;
107
108pub const MAX_SKETCH_CHECKPOINTS: usize = 100;
109
110#[derive(Debug, Clone)]
111struct SketchCheckpoint {
112    id: SketchCheckpointId,
113    source: SourceDelta,
114    program: Program,
115    scene_graph: SceneGraph,
116    exec_outcome: ExecOutcome,
117    point_freedom_cache: HashMap<ObjectId, Freedom>,
118    mock_memory: Option<SketchModeState>,
119}
120pub(crate) mod trim;
121
122struct ArcSizeConstraintParams {
123    points: Vec<ObjectId>,
124    function_name: &'static str,
125    value: f64,
126    units: NumericSuffix,
127    label_position: Option<Point2d<Number>>,
128    constraint_type_name: &'static str,
129}
130
131const POINT_FN: &str = "point";
132const POINT_AT_PARAM: &str = "at";
133const LINE_FN: &str = "line";
134const LINE_VARIABLE: &str = "line";
135const LINE_START_PARAM: &str = "start";
136const LINE_END_PARAM: &str = "end";
137const ARC_FN: &str = "arc";
138const ARC_VARIABLE: &str = "arc";
139const ARC_START_PARAM: &str = "start";
140const ARC_END_PARAM: &str = "end";
141const ARC_CENTER_PARAM: &str = "center";
142const ARC_DIRECTION_PARAM: &str = "direction";
143/// The name of the KCL std constant for a clockwise arc direction.
144const ARC_DIRECTION_CW_NAME: &str = "CW";
145const CIRCLE_FN: &str = "circle";
146const CIRCLE_VARIABLE: &str = "circle";
147const CIRCLE_START_PARAM: &str = "start";
148const CIRCLE_CENTER_PARAM: &str = "center";
149const CONTROL_POINT_SPLINE_FN: &str = "controlPointSpline";
150const CONTROL_POINT_SPLINE_POINTS_PARAM: &str = "points";
151const LABEL_POSITION_PARAM: &str = "labelPosition";
152
153const COINCIDENT_FN: &str = "coincident";
154const DIAMETER_FN: &str = "diameter";
155const DISTANCE_FN: &str = "distance";
156const FIXED_FN: &str = "fixed";
157const ANGLE_FN: &str = "angle";
158const ANGLE_DIMENSION_FN: &str = "angleDimension";
159const ANGLE_LINES_PARAM: &str = "lines";
160const ANGLE_SECTOR_PARAM: &str = "sector";
161const ANGLE_INVERSE_PARAM: &str = "inverse";
162const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
163const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
164const EQUAL_LENGTH_FN: &str = "equalLength";
165const EQUAL_RADIUS_FN: &str = "equalRadius";
166const HORIZONTAL_FN: &str = "horizontal";
167const MIDPOINT_FN: &str = "midpoint";
168const MIDPOINT_POINT_PARAM: &str = "point";
169const RADIUS_FN: &str = "radius";
170const SYMMETRIC_FN: &str = "symmetric";
171const SYMMETRIC_AXIS_PARAM: &str = "axis";
172const TANGENT_FN: &str = "tangent";
173const VERTICAL_FN: &str = "vertical";
174
175const LINE_PROPERTY_START: &str = "start";
176const LINE_PROPERTY_END: &str = "end";
177
178const ARC_PROPERTY_START: &str = "start";
179const ARC_PROPERTY_END: &str = "end";
180const ARC_PROPERTY_CENTER: &str = "center";
181const CIRCLE_PROPERTY_START: &str = "start";
182const CIRCLE_PROPERTY_CENTER: &str = "center";
183const CONTROL_POINT_SPLINE_PROPERTY_CONTROLS: &str = "controls";
184const CONTROL_POINT_SPLINE_PROPERTY_EDGES: &str = "edges";
185
186const CONSTRUCTION_PARAM: &str = "construction";
187
188#[derive(Debug, Clone, Copy)]
189enum EditDeleteKind {
190    Edit,
191    DeleteNonSketch,
192}
193
194/// Options that control how an edit is re-executed and written back.
195struct ExecuteAfterEditOptions {
196    segment_ids_edited: AhashIndexSet<ObjectId>,
197    edit_kind: EditDeleteKind,
198    commit_solved_initial_guesses: bool,
199}
200
201impl EditDeleteKind {
202    /// Returns true if this edit is any type of deletion.
203    fn is_delete(&self) -> bool {
204        match self {
205            EditDeleteKind::Edit => false,
206            EditDeleteKind::DeleteNonSketch => true,
207        }
208    }
209
210    fn to_change_kind(self) -> ChangeKind {
211        match self {
212            EditDeleteKind::Edit => ChangeKind::Edit,
213            EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
214        }
215    }
216}
217
218#[derive(Debug, Clone, Copy)]
219enum ChangeKind {
220    Add,
221    Edit,
222    Delete,
223    None,
224}
225
226#[derive(Debug, Clone, Serialize, ts_rs::TS)]
227#[ts(export, export_to = "FrontendApi.ts")]
228#[serde(tag = "type")]
229pub enum SetProgramOutcome {
230    #[serde(rename_all = "camelCase")]
231    Success {
232        scene_graph: Box<SceneGraph>,
233        exec_outcome: Box<ExecOutcome>,
234        checkpoint_id: Option<SketchCheckpointId>,
235    },
236    #[serde(rename_all = "camelCase")]
237    ExecFailure { error: Box<KclErrorWithOutputs> },
238}
239
240/// Options for a sketch segment edit that participates in drag solving.
241pub struct EditSegmentsOptions {
242    /// Narrows which edited scene objects receive temporary fixed constraints.
243    ///
244    /// `None` keeps the default of anchoring every edited segment. `Some(vec![])`
245    /// disables those fixed constraints, which is useful for semantic edits such
246    /// as toggling construction state.
247    pub anchor_segment_ids: Option<Vec<ObjectId>>,
248    /// Hidden fixed cursor points that the referenced segment bodies must pass
249    /// through during solve.
250    pub drag_anchors: Vec<SegmentDragAnchor>,
251    /// Constraint label positions to write in the same AST transaction as the
252    /// segment edits. Label positions do not participate in solving.
253    pub constraint_label_edits: Vec<ConstraintLabelPositionEdit>,
254    /// Whether solver-updated initial guesses should be written back to KCL.
255    pub commit_solved_initial_guesses: bool,
256}
257
258/// Options for a distance-constraint label edit during sketch dragging.
259pub struct EditDistanceConstraintLabelPositionOptions {
260    /// Edited scene objects to keep anchored while previewing the label edit.
261    pub anchor_segment_ids: Vec<ObjectId>,
262    /// Whether solver-updated initial guesses should be written back to KCL.
263    pub commit_solved_initial_guesses: bool,
264}
265
266/// Options for editing a constraint during sketch dragging.
267pub struct EditConstraintOptions {
268    /// Whether solver-updated initial guesses should be written back to KCL.
269    pub commit_solved_initial_guesses: bool,
270}
271
272#[derive(Debug, Clone)]
273struct SolidAstReference {
274    variable_name: String,
275    output_index: Option<usize>,
276}
277
278#[derive(Debug, Clone)]
279pub struct FrontendState {
280    program: Program,
281    scene_graph: SceneGraph,
282    /// Lightweight map from engine solid IDs to their latest KCL references.
283    solid_references: HashMap<Uuid, SolidAstReference>,
284    /// Stores the last known freedom value for each point object.
285    /// This allows us to preserve freedom values when freedom analysis isn't run.
286    point_freedom_cache: HashMap<ObjectId, Freedom>,
287    /// One-shot drag anchors for the next segment edit. These ids define which
288    /// edited points/segments become temporary fixed constraints during solve.
289    next_drag_anchor_segment_ids: Option<AhashIndexSet<ObjectId>>,
290    /// One-shot segment-body drag anchors for the next segment edit. These add
291    /// a temporary solver point on the dragged segment that follows the cursor.
292    next_segment_drag_anchors: Option<Vec<SegmentDragAnchor>>,
293    /// One-shot constraint label edits to apply with the next segment edit.
294    next_constraint_label_edits: Option<Vec<ConstraintLabelPositionEdit>>,
295    /// One-shot override for whether the next edit commits solver-updated
296    /// initial guesses back into KCL. Drag previews keep this off so only the
297    /// explicit drag edit feeds the next solve.
298    next_edit_commits_solver_solutions: Option<bool>,
299    sketch_checkpoints: VecDeque<SketchCheckpoint>,
300    sketch_checkpoint_id_gen: IncIdGenerator<u64>,
301}
302
303impl Default for FrontendState {
304    fn default() -> Self {
305        Self::new()
306    }
307}
308
309impl FrontendState {
310    pub fn new() -> Self {
311        Self {
312            program: Program::empty(),
313            scene_graph: SceneGraph {
314                project: ProjectId(0),
315                file: FileId(0),
316                version: Version(0),
317                objects: Default::default(),
318                settings: Default::default(),
319                sketch_mode: Default::default(),
320            },
321            solid_references: HashMap::new(),
322            point_freedom_cache: HashMap::new(),
323            next_drag_anchor_segment_ids: None,
324            next_segment_drag_anchors: None,
325            next_constraint_label_edits: None,
326            next_edit_commits_solver_solutions: None,
327            sketch_checkpoints: VecDeque::new(),
328            sketch_checkpoint_id_gen: IncIdGenerator::new(1),
329        }
330    }
331
332    /// Get a reference to the scene graph
333    pub fn scene_graph(&self) -> &SceneGraph {
334        &self.scene_graph
335    }
336
337    pub fn default_length_unit(&self) -> UnitLength {
338        self.program
339            .meta_settings()
340            .ok()
341            .flatten()
342            .map(|settings| settings.default_length_units)
343            .unwrap_or(UnitLength::Millimeters)
344    }
345
346    pub async fn create_sketch_checkpoint(&mut self, exec_outcome: ExecOutcome) -> api::Result<SketchCheckpointId> {
347        let checkpoint_id = SketchCheckpointId::new(self.sketch_checkpoint_id_gen.next_id());
348
349        let checkpoint = SketchCheckpoint {
350            id: checkpoint_id,
351            source: SourceDelta {
352                text: source_from_ast(&self.program.ast),
353            },
354            program: self.program.clone(),
355            scene_graph: self.scene_graph.clone(),
356            exec_outcome,
357            point_freedom_cache: self.point_freedom_cache.clone(),
358            mock_memory: read_old_memory().await,
359        };
360
361        self.sketch_checkpoints.push_back(checkpoint);
362        while self.sketch_checkpoints.len() > MAX_SKETCH_CHECKPOINTS {
363            self.sketch_checkpoints.pop_front();
364        }
365
366        Ok(checkpoint_id)
367    }
368
369    /// Edit sketch segments with optional drag-solve overrides.
370    ///
371    /// Drag anchors add hidden fixed cursor points and constrain the referenced
372    /// segment bodies to pass through them, which lets body drags use the same
373    /// anchor model without pinning all child points. Preview callers disable
374    /// solver writeback so solved geometry can be returned without feeding every
375    /// solver value back into KCL.
376    pub async fn edit_segments_with_options(
377        &mut self,
378        ctx: &ExecutorContext,
379        version: Version,
380        sketch: ObjectId,
381        segments: Vec<ExistingSegmentCtor>,
382        options: EditSegmentsOptions,
383    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
384        let previous_anchor_ids = options.anchor_segment_ids.map(|anchor_ids| {
385            self.next_drag_anchor_segment_ids
386                .replace(anchor_ids.into_iter().collect())
387        });
388        let previous_drag_anchors = self.next_segment_drag_anchors.replace(options.drag_anchors);
389        let previous_constraint_label_edits = self.next_constraint_label_edits.replace(options.constraint_label_edits);
390        let previous_commit_mode = self
391            .next_edit_commits_solver_solutions
392            .replace(options.commit_solved_initial_guesses);
393        let result = SketchApi::edit_segments(self, ctx, version, sketch, segments).await;
394        if let Some(previous_anchor_ids) = previous_anchor_ids {
395            self.next_drag_anchor_segment_ids = previous_anchor_ids;
396        }
397        self.next_segment_drag_anchors = previous_drag_anchors;
398        self.next_constraint_label_edits = previous_constraint_label_edits;
399        self.next_edit_commits_solver_solutions = previous_commit_mode;
400        result
401    }
402
403    /// Edit a distance-constraint label position with optional solver writeback.
404    ///
405    /// Drag previews set `commit_solved_initial_guesses` to false so label
406    /// placement can be previewed against solved geometry without advancing
407    /// persistent KCL state until drag completion.
408    pub async fn edit_distance_constraint_label_position_with_options(
409        &mut self,
410        ctx: &ExecutorContext,
411        version: Version,
412        sketch: ObjectId,
413        constraint_id: ObjectId,
414        label_position: Point2d<Number>,
415        options: EditDistanceConstraintLabelPositionOptions,
416    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
417        let previous_commit_mode = self
418            .next_edit_commits_solver_solutions
419            .replace(options.commit_solved_initial_guesses);
420        let result = SketchApi::edit_distance_constraint_label_position(
421            self,
422            ctx,
423            version,
424            sketch,
425            constraint_id,
426            label_position,
427            options.anchor_segment_ids,
428        )
429        .await;
430        self.next_edit_commits_solver_solutions = previous_commit_mode;
431        result
432    }
433
434    /// Edit a distance constraint with optional solver writeback.
435    pub async fn edit_distance_constraint_with_options(
436        &mut self,
437        ctx: &ExecutorContext,
438        version: Version,
439        sketch: ObjectId,
440        constraint_id: ObjectId,
441        constraint: Constraint,
442        options: EditConstraintOptions,
443    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
444        self.edit_constraint_with_options(ctx, version, sketch, constraint_id, constraint, options)
445            .await
446    }
447
448    /// Edit an angle constraint with optional solver writeback.
449    pub async fn edit_angle_constraint_with_options(
450        &mut self,
451        ctx: &ExecutorContext,
452        version: Version,
453        sketch: ObjectId,
454        constraint_id: ObjectId,
455        angle: Angle,
456        options: EditConstraintOptions,
457    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
458        self.edit_constraint_with_options(ctx, version, sketch, constraint_id, Constraint::Angle(angle), options)
459            .await
460    }
461
462    /// Edit an angle or distance-family constraint with optional solver writeback.
463    pub async fn edit_constraint_with_options(
464        &mut self,
465        ctx: &ExecutorContext,
466        _version: Version,
467        sketch: ObjectId,
468        constraint_id: ObjectId,
469        constraint: Constraint,
470        options: EditConstraintOptions,
471    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
472        // TODO: Check version.
473        let sketch_block_ref =
474            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
475
476        let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
477            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
478        })?;
479
480        let mut new_ast = self.program.ast.clone();
481        let command = match &object.kind {
482            ObjectKind::Constraint {
483                constraint:
484                    Constraint::Distance(_) | Constraint::HorizontalDistance(_) | Constraint::VerticalDistance(_),
485            } => {
486                let (function_name, distance) = match &constraint {
487                    Constraint::Distance(distance) => (DISTANCE_FN, distance),
488                    Constraint::HorizontalDistance(distance) => (HORIZONTAL_DISTANCE_FN, distance),
489                    Constraint::VerticalDistance(distance) => (VERTICAL_DISTANCE_FN, distance),
490                    _ => {
491                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
492                            "A distance constraint can only be replaced by another distance constraint".to_owned(),
493                        )));
494                    }
495                };
496                let (call, value) = self
497                    .distance_constraint_ast_parts(function_name, distance, &mut new_ast)
498                    .map_err(KclErrorWithOutputs::no_outputs)?;
499                AstMutateCommand::EditDistanceConstraint { call, value }
500            }
501            ObjectKind::Constraint {
502                constraint: Constraint::Angle(_),
503            } => {
504                let Constraint::Angle(angle) = &constraint else {
505                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
506                        "An angle constraint can only be replaced by another angle constraint".to_owned(),
507                    )));
508                };
509                let (call, value) = self
510                    .angle_constraint_ast_parts(angle, &mut new_ast)
511                    .map_err(KclErrorWithOutputs::no_outputs)?;
512                AstMutateCommand::EditAngleConstraint { call, value }
513            }
514            ObjectKind::Constraint { .. } => {
515                return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
516                    "Editing {} is not supported",
517                    object.kind.human_friendly_kind_with_article(),
518                ))));
519            }
520            _ => {
521                return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
522                    "Object is not a constraint: {constraint_id:?}"
523                ))));
524            }
525        };
526
527        self.mutate_ast(&mut new_ast, constraint_id, command)
528            .map_err(KclErrorWithOutputs::no_outputs)?;
529
530        self.execute_after_edit(
531            ctx,
532            sketch,
533            sketch_block_ref,
534            &mut new_ast,
535            ExecuteAfterEditOptions {
536                segment_ids_edited: Default::default(),
537                edit_kind: EditDeleteKind::Edit,
538                commit_solved_initial_guesses: options.commit_solved_initial_guesses,
539            },
540        )
541        .await
542    }
543
544    pub async fn restore_sketch_checkpoint(
545        &mut self,
546        checkpoint_id: SketchCheckpointId,
547    ) -> api::Result<RestoreSketchCheckpointOutcome> {
548        let checkpoint = self
549            .sketch_checkpoints
550            .iter()
551            .find(|checkpoint| checkpoint.id == checkpoint_id)
552            .cloned()
553            .ok_or_else(|| Error {
554                msg: format!("Sketch checkpoint not found: {checkpoint_id:?}"),
555            })?;
556
557        self.program = checkpoint.program;
558        self.scene_graph = checkpoint.scene_graph.clone();
559        self.solid_references = solid_references_from_variables(&self.program.ast, &checkpoint.exec_outcome.variables);
560        self.point_freedom_cache = checkpoint.point_freedom_cache;
561        self.next_drag_anchor_segment_ids = None;
562        self.next_segment_drag_anchors = None;
563        self.next_constraint_label_edits = None;
564        self.next_edit_commits_solver_solutions = None;
565
566        if let Some(mock_memory) = checkpoint.mock_memory {
567            write_old_memory(mock_memory).await;
568        } else {
569            clear_mem_cache().await;
570        }
571
572        Ok(RestoreSketchCheckpointOutcome {
573            source_delta: checkpoint.source,
574            scene_graph_delta: SceneGraphDelta {
575                new_graph: self.scene_graph_for_ui(),
576                new_objects: Vec::new(),
577                invalidates_ids: true,
578                exec_outcome: checkpoint.exec_outcome,
579            },
580        })
581    }
582
583    pub fn clear_sketch_checkpoints(&mut self) {
584        self.sketch_checkpoints.clear();
585    }
586    fn scene_graph_for_ui(&self) -> SceneGraph {
587        let has_control_point_splines = self.scene_graph.objects.iter().any(|object| {
588            matches!(
589                object.kind,
590                ObjectKind::Segment {
591                    segment: Segment::ControlPointSpline(_)
592                }
593            )
594        });
595
596        if !has_control_point_splines {
597            return self.scene_graph.clone();
598        }
599
600        let hidden_constraint_ids = self
601            .scene_graph
602            .objects
603            .iter()
604            .filter_map(|object| match &object.kind {
605                ObjectKind::Constraint {
606                    constraint: Constraint::Coincident(coincident),
607                } if coincident_is_internal_to_same_control_point_spline(coincident, &self.scene_graph) => {
608                    Some(object.id)
609                }
610                _ => None,
611            })
612            .collect::<HashSet<_>>();
613
614        if hidden_constraint_ids.is_empty() {
615            return self.scene_graph.clone();
616        }
617
618        let mut scene_graph = self.scene_graph.clone();
619        for object in &mut scene_graph.objects {
620            match &mut object.kind {
621                ObjectKind::Constraint { .. } if hidden_constraint_ids.contains(&object.id) => {
622                    object.kind = ObjectKind::Nil;
623                }
624                ObjectKind::Sketch(sketch) => {
625                    sketch
626                        .constraints
627                        .retain(|constraint_id| !hidden_constraint_ids.contains(constraint_id));
628                }
629                _ => {}
630            }
631        }
632
633        scene_graph
634    }
635}
636
637fn coincident_is_internal_to_same_control_point_spline(coincident: &Coincident, scene_graph: &SceneGraph) -> bool {
638    let mut first_owner_id = None;
639    for segment_id in coincident.segment_ids() {
640        let Some(owner_id) = owning_control_point_spline_id(segment_id, scene_graph) else {
641            return false;
642        };
643
644        match first_owner_id {
645            Some(first_owner_id) if first_owner_id != owner_id => return false,
646            Some(_) => {}
647            None => first_owner_id = Some(owner_id),
648        }
649    }
650
651    first_owner_id.is_some()
652}
653
654fn owning_control_point_spline_id(segment_id: ObjectId, scene_graph: &SceneGraph) -> Option<ObjectId> {
655    let object = scene_graph.objects.get(segment_id.0)?;
656    let ObjectKind::Segment { segment } = &object.kind else {
657        return None;
658    };
659
660    match segment {
661        Segment::ControlPointSpline(_) => Some(segment_id),
662        Segment::Point(point) => point
663            .owner
664            .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
665        Segment::Line(line) => line
666            .owner
667            .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
668        _ => None,
669    }
670}
671
672fn matches_control_point_spline_owner(owner_id: ObjectId, scene_graph: &SceneGraph) -> bool {
673    matches!(
674        scene_graph.objects.get(owner_id.0).map(|object| &object.kind),
675        Some(ObjectKind::Segment {
676            segment: Segment::ControlPointSpline(_)
677        })
678    )
679}
680
681fn ensure_control_point_spline_experimental_features(program: &Program) -> Result<Program, KclError> {
682    let experimental_features_allowed = program
683        .meta_settings()
684        .ok()
685        .flatten()
686        .map(|settings| settings.experimental_features == WarningLevel::Allow)
687        .unwrap_or(false);
688    if experimental_features_allowed {
689        return Ok(program.clone());
690    }
691
692    program.change_experimental_features(Some(WarningLevel::Allow))
693}
694
695impl SketchApi for FrontendState {
696    async fn execute_mock(
697        &mut self,
698        ctx: &ExecutorContext,
699        _version: Version,
700        sketch: ObjectId,
701    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
702        let sketch_block_ref =
703            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
704
705        let mut truncated_program = self.program.clone();
706        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
707            .map_err(KclErrorWithOutputs::no_outputs)?;
708
709        // Execute.
710        let outcome = ctx
711            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
712            .await?;
713        let new_source = source_from_ast(&self.program.ast);
714        let src_delta = SourceDelta { text: new_source };
715        // MockConfig::default() has freedom_analysis: true
716        let outcome = self.update_state_after_exec(outcome, true);
717        let scene_graph_delta = SceneGraphDelta {
718            new_graph: self.scene_graph.clone(),
719            new_objects: Default::default(),
720            invalidates_ids: false,
721            exec_outcome: outcome,
722        };
723        Ok((src_delta, scene_graph_delta))
724    }
725
726    async fn new_sketch(
727        &mut self,
728        ctx: &ExecutorContext,
729        _project: ProjectId,
730        _file: FileId,
731        _version: Version,
732        args: SketchCtor,
733    ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)> {
734        // TODO: Check version.
735
736        let mut new_ast = self.program.ast.clone();
737        // Create updated KCL source from args.
738        let mut plane_ast = sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &self.solid_references, &args.on)
739            .map_err(KclErrorWithOutputs::no_outputs)?;
740        let mut defined_names = find_defined_names(&new_ast);
741        let is_face_of_expr = matches!(
742            &plane_ast,
743            ast::Expr::CallExpressionKw(call) if call.callee.name.name == "faceOf"
744        );
745        if is_face_of_expr {
746            let face_name = next_free_name_with_padding("face", &defined_names)
747                .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
748            let face_decl = ast::VariableDeclaration::new(
749                ast::VariableDeclarator::new(&face_name, plane_ast),
750                ast::ItemVisibility::Default,
751                ast::VariableKind::Const,
752            );
753            new_ast
754                .body
755                .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
756                    face_decl,
757                ))));
758            defined_names.insert(face_name.clone());
759            plane_ast = ast::Expr::Name(BoxNode::new(ast::Name::new(&face_name)));
760        }
761        let sketch_ast = ast::SketchBlock {
762            arguments: vec![ast::LabeledArg {
763                label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
764                arg: plane_ast,
765            }],
766            body: Default::default(),
767            is_being_edited: false,
768            non_code_meta: Default::default(),
769            digest: None,
770        };
771        // Add a sketch block as a variable declaration directly, avoiding
772        // source-range mutation on a no-src node.
773        let sketch_name = next_free_name_with_padding("sketch", &defined_names)
774            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
775        let sketch_decl = ast::VariableDeclaration::new(
776            ast::VariableDeclarator::new(
777                &sketch_name,
778                ast::Expr::SketchBlock(BoxNode::new(ast::Node::no_src(sketch_ast))),
779            ),
780            ast::ItemVisibility::Default,
781            ast::VariableKind::Const,
782        );
783        new_ast
784            .body
785            .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
786                sketch_decl,
787            ))));
788        // Convert to string source to create real source ranges.
789        let new_source = source_from_ast(&new_ast);
790        // Parse the new source.
791        let new_program = parse_frontend_mutation_source(
792            &new_source,
793            "Error parsing KCL source after adding sketch",
794            "No AST produced after adding sketch",
795        )?;
796
797        // Make sure to only set this if there are no errors.
798        self.program = new_program.clone();
799
800        // We need to do an engine execute so that the plane object gets created
801        // and is cached.
802        let outcome = ctx.run_with_caching(new_program.clone()).await?;
803        let freedom_analysis_ran = true;
804
805        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
806
807        let Some(sketch_id) = self
808            .scene_graph
809            .objects
810            .iter()
811            .filter_map(|object| match object.kind {
812                ObjectKind::Sketch(_) => Some(object.id),
813                _ => None,
814            })
815            .max_by_key(|id| id.0)
816        else {
817            return Err(KclErrorWithOutputs::from_error_outcome(
818                KclError::refactor("No objects in scene graph after adding sketch".to_owned()),
819                outcome,
820            ));
821        };
822        // Store the object in the scene.
823        self.scene_graph.sketch_mode = Some(sketch_id);
824
825        let src_delta = SourceDelta { text: new_source };
826        let scene_graph_delta = SceneGraphDelta {
827            new_graph: self.scene_graph_for_ui(),
828            invalidates_ids: false,
829            new_objects: vec![sketch_id],
830            exec_outcome: outcome,
831        };
832        Ok((src_delta, scene_graph_delta, sketch_id))
833    }
834
835    async fn edit_sketch(
836        &mut self,
837        ctx: &ExecutorContext,
838        _project: ProjectId,
839        _file: FileId,
840        _version: Version,
841        sketch: ObjectId,
842    ) -> ExecResult<SceneGraphDelta> {
843        // TODO: Check version.
844
845        // Look up existing sketch.
846        let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
847            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
848        })?;
849        let ObjectKind::Sketch(_) = &sketch_object.kind else {
850            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
851                "Object is not a sketch, it is {}",
852                sketch_object.kind.human_friendly_kind_with_article()
853            ))));
854        };
855        let sketch_block_ref = expect_single_node_ref(sketch_object).map_err(KclErrorWithOutputs::no_outputs)?;
856
857        // Enter sketch mode by setting the sketch_mode.
858        self.scene_graph.sketch_mode = Some(sketch);
859
860        // Truncate after the sketch block for mock execution.
861        let mut truncated_program = self.program.clone();
862        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
863            .map_err(KclErrorWithOutputs::no_outputs)?;
864
865        // Execute in mock mode to ensure state is up to date. The caller will
866        // want freedom analysis to display segments correctly.
867        let outcome = ctx
868            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
869            .await?;
870
871        // MockConfig::default() has freedom_analysis: true
872        let outcome = self.update_state_after_exec(outcome, true);
873        let scene_graph_delta = SceneGraphDelta {
874            new_graph: self.scene_graph_for_ui(),
875            invalidates_ids: false,
876            new_objects: Vec::new(),
877            exec_outcome: outcome,
878        };
879        Ok(scene_graph_delta)
880    }
881
882    async fn exit_sketch(
883        &mut self,
884        ctx: &ExecutorContext,
885        _version: Version,
886        sketch: ObjectId,
887    ) -> ExecResult<SceneGraph> {
888        // TODO: Check version.
889        #[cfg(not(target_arch = "wasm32"))]
890        let _ = sketch;
891        #[cfg(target_arch = "wasm32")]
892        if self.scene_graph.sketch_mode != Some(sketch) {
893            web_sys::console::warn_1(
894                &format!(
895                    "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
896                    self.scene_graph.sketch_mode
897                )
898                .into(),
899            );
900        }
901        self.scene_graph.sketch_mode = None;
902
903        // Execute.
904        let outcome = ctx.run_with_caching(self.program.clone()).await?;
905
906        // exit_sketch doesn't run freedom analysis, just clears sketch_mode
907        self.update_state_after_exec(outcome, false);
908
909        Ok(self.scene_graph_for_ui())
910    }
911
912    async fn delete_sketch(
913        &mut self,
914        ctx: &ExecutorContext,
915        _version: Version,
916        sketch: ObjectId,
917    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
918        // TODO: Check version.
919
920        let mut new_ast = self.program.ast.clone();
921
922        // Look up existing sketch.
923        let sketch_id = sketch;
924        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
925            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
926        })?;
927        let ObjectKind::Sketch(_) = &sketch_object.kind else {
928            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
929                "Object is not a sketch, it is {}",
930                sketch_object.kind.human_friendly_kind_with_article(),
931            ))));
932        };
933
934        // Modify the AST to remove the sketch.
935        self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)
936            .map_err(KclErrorWithOutputs::no_outputs)?;
937
938        self.execute_after_delete_sketch(ctx, &mut new_ast).await
939    }
940
941    async fn add_segment(
942        &mut self,
943        ctx: &ExecutorContext,
944        _version: Version,
945        sketch: ObjectId,
946        segment: SegmentCtor,
947        _label: Option<String>,
948    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
949        // TODO: Check version.
950        match segment {
951            SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
952            SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
953            SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
954            SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
955            SegmentCtor::ControlPointSpline(ctor) => self.add_control_point_spline(ctx, sketch, ctor).await,
956        }
957    }
958
959    async fn edit_segments(
960        &mut self,
961        ctx: &ExecutorContext,
962        _version: Version,
963        sketch: ObjectId,
964        segments: Vec<ExistingSegmentCtor>,
965    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
966        // TODO: Check version.
967        let sketch_block_ref =
968            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
969
970        let mut new_ast = self.program.ast.clone();
971        let mut edited_segment_ids = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
972        let mut invalidates_ids = false;
973
974        // edited_segment_ids still has to be the original segments (not final_edits), otherwise the owner segments
975        // are passed to `execute_after_edit` which changes the result of the solver, causing tests to fail.
976        for segment in &segments {
977            edited_segment_ids.insert(segment.id);
978            if let SegmentCtor::ControlPointSpline(new_ctor) = &segment.ctor
979                && let Some(existing_object) = self.scene_graph.objects.get(segment.id.0)
980                && let ObjectKind::Segment {
981                    segment: Segment::ControlPointSpline(existing_spline),
982                } = &existing_object.kind
983                && existing_spline.controls.len() != new_ctor.points.len()
984            {
985                invalidates_ids = true;
986            }
987        }
988        let drag_anchor_segment_ids = self
989            .next_drag_anchor_segment_ids
990            .take()
991            .unwrap_or_else(|| edited_segment_ids.clone());
992        let constraint_label_edits = self.next_constraint_label_edits.take().unwrap_or_default();
993        let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
994
995        // Preprocess segments into a final_edits vector to handle if segments contains:
996        // - edit start point of line1 (as SegmentCtor::Point)
997        // - edit end point of line1 (as SegmentCtor::Point)
998        //
999        // This would result in only the end point to be updated because edit_point() clones line1's ctor from
1000        // scene_graph, but this is still the old ctor because self.scene_graph is only updated after the loop finishes.
1001        //
1002        // To fix this, and other cases when the same point is edited from multiple elements in the segments Vec
1003        // we apply all edits in order to final_edits in a way that owned point edits result in line edits,
1004        // so the above example would result in a single line1 edit:
1005        // - the first start point edit creates a new line edit entry in final_edits
1006        // - the second end point edit finds this line edit and mutates the end position only.
1007        //
1008        // The result is that segments are flattened into a single IndexMap of edits by their owners, later edits overriding earlier ones.
1009        let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
1010
1011        for segment in segments {
1012            let segment_id = segment.id;
1013            match segment.ctor {
1014                SegmentCtor::Point(ctor) => {
1015                    // Find the owner, if any (point -> line / arc)
1016                    if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
1017                        && let ObjectKind::Segment { segment } = &segment_object.kind
1018                        && let Segment::Point(point) = segment
1019                        && let Some(owner_id) = point.owner
1020                        && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
1021                        && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
1022                    {
1023                        match owner_segment {
1024                            Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
1025                                if let Some(existing) = final_edits.get_mut(&owner_id) {
1026                                    let SegmentCtor::Line(line_ctor) = existing else {
1027                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1028                                            "Internal: Expected line ctor for owner, but found {}",
1029                                            existing.human_friendly_kind_with_article()
1030                                        ))));
1031                                    };
1032                                    // Line owner is already in final_edits -> apply this point edit
1033                                    if line.start == segment_id {
1034                                        line_ctor.start = ctor.position;
1035                                    } else {
1036                                        line_ctor.end = ctor.position;
1037                                    }
1038                                } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
1039                                    // Line owner is not in final_edits yet -> create it
1040                                    let mut line_ctor = line_ctor.clone();
1041                                    if line.start == segment_id {
1042                                        line_ctor.start = ctor.position;
1043                                    } else {
1044                                        line_ctor.end = ctor.position;
1045                                    }
1046                                    final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
1047                                } else {
1048                                    // This should never run..
1049                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1050                                        "Internal: Line does not have line ctor, but found {}",
1051                                        line.ctor.human_friendly_kind_with_article()
1052                                    ))));
1053                                }
1054                                continue;
1055                            }
1056                            Segment::Arc(arc)
1057                                if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
1058                            {
1059                                if let Some(existing) = final_edits.get_mut(&owner_id) {
1060                                    let SegmentCtor::Arc(arc_ctor) = existing else {
1061                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1062                                            "Internal: Expected arc ctor for owner, but found {}",
1063                                            existing.human_friendly_kind_with_article()
1064                                        ))));
1065                                    };
1066                                    if arc.start == segment_id {
1067                                        arc_ctor.start = ctor.position;
1068                                    } else if arc.end == segment_id {
1069                                        arc_ctor.end = ctor.position;
1070                                    } else {
1071                                        arc_ctor.center = ctor.position;
1072                                    }
1073                                } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
1074                                    let mut arc_ctor = arc_ctor.clone();
1075                                    if arc.start == segment_id {
1076                                        arc_ctor.start = ctor.position;
1077                                    } else if arc.end == segment_id {
1078                                        arc_ctor.end = ctor.position;
1079                                    } else {
1080                                        arc_ctor.center = ctor.position;
1081                                    }
1082                                    final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
1083                                } else {
1084                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1085                                        "Internal: Arc does not have arc ctor, but found {}",
1086                                        arc.ctor.human_friendly_kind_with_article()
1087                                    ))));
1088                                }
1089                                continue;
1090                            }
1091                            Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
1092                                if let Some(existing) = final_edits.get_mut(&owner_id) {
1093                                    let SegmentCtor::Circle(circle_ctor) = existing else {
1094                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1095                                            "Internal: Expected circle ctor for owner, but found {}",
1096                                            existing.human_friendly_kind_with_article()
1097                                        ))));
1098                                    };
1099                                    if circle.start == segment_id {
1100                                        circle_ctor.start = ctor.position;
1101                                    } else {
1102                                        circle_ctor.center = ctor.position;
1103                                    }
1104                                } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
1105                                    let mut circle_ctor = circle_ctor.clone();
1106                                    if circle.start == segment_id {
1107                                        circle_ctor.start = ctor.position;
1108                                    } else {
1109                                        circle_ctor.center = ctor.position;
1110                                    }
1111                                    final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
1112                                } else {
1113                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1114                                        "Internal: Circle does not have circle ctor, but found {}",
1115                                        circle.ctor.human_friendly_kind_with_article()
1116                                    ))));
1117                                }
1118                                continue;
1119                            }
1120                            Segment::ControlPointSpline(spline) if spline.controls.contains(&segment_id) => {
1121                                let Some(control_index) =
1122                                    spline.controls.iter().position(|control_id| *control_id == segment_id)
1123                                else {
1124                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1125                                        "Internal: Point is not part of owner's controlPointSpline segment: point={segment_id:?}, spline={owner_id:?}"
1126                                    ))));
1127                                };
1128                                if let Some(existing) = final_edits.get_mut(&owner_id) {
1129                                    let SegmentCtor::ControlPointSpline(spline_ctor) = existing else {
1130                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1131                                            "Internal: Expected controlPointSpline ctor for owner, but found {}",
1132                                            existing.human_friendly_kind_with_article()
1133                                        ))));
1134                                    };
1135                                    spline_ctor.points[control_index] = ctor.position;
1136                                } else if let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor {
1137                                    let mut spline_ctor = spline_ctor.clone();
1138                                    spline_ctor.points[control_index] = ctor.position;
1139                                    final_edits.insert(owner_id, SegmentCtor::ControlPointSpline(spline_ctor));
1140                                } else {
1141                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1142                                        "Internal: Control point spline does not have controlPointSpline ctor, but found {}",
1143                                        spline.ctor.human_friendly_kind_with_article()
1144                                    ))));
1145                                }
1146                                continue;
1147                            }
1148                            _ => {}
1149                        }
1150                    }
1151
1152                    // No owner, it's an individual point
1153                    final_edits.insert(segment_id, SegmentCtor::Point(ctor));
1154                }
1155                SegmentCtor::Line(ctor) => {
1156                    final_edits.insert(segment_id, SegmentCtor::Line(ctor));
1157                }
1158                SegmentCtor::Arc(ctor) => {
1159                    final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
1160                }
1161                SegmentCtor::Circle(ctor) => {
1162                    final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
1163                }
1164                SegmentCtor::ControlPointSpline(ctor) => {
1165                    final_edits.insert(segment_id, SegmentCtor::ControlPointSpline(ctor));
1166                }
1167            }
1168        }
1169
1170        for (segment_id, ctor) in final_edits {
1171            match ctor {
1172                SegmentCtor::Point(ctor) => self
1173                    .edit_point(&mut new_ast, sketch, segment_id, ctor)
1174                    .map_err(KclErrorWithOutputs::no_outputs)?,
1175                SegmentCtor::Line(ctor) => self
1176                    .edit_line(&mut new_ast, sketch, segment_id, ctor)
1177                    .map_err(KclErrorWithOutputs::no_outputs)?,
1178                SegmentCtor::Arc(ctor) => self
1179                    .edit_arc(&mut new_ast, sketch, segment_id, ctor)
1180                    .map_err(KclErrorWithOutputs::no_outputs)?,
1181                SegmentCtor::Circle(ctor) => self
1182                    .edit_circle(&mut new_ast, sketch, segment_id, ctor)
1183                    .map_err(KclErrorWithOutputs::no_outputs)?,
1184                SegmentCtor::ControlPointSpline(ctor) => self
1185                    .edit_control_point_spline(&mut new_ast, sketch, segment_id, ctor)
1186                    .map_err(KclErrorWithOutputs::no_outputs)?,
1187            }
1188        }
1189        for edit in constraint_label_edits {
1190            self.mutate_constraint_label_position(&mut new_ast, edit.constraint_id, edit.label_position)
1191                .map_err(KclErrorWithOutputs::no_outputs)?;
1192        }
1193        let (source_delta, mut scene_graph_delta) = self
1194            .execute_after_edit(
1195                ctx,
1196                sketch,
1197                sketch_block_ref,
1198                &mut new_ast,
1199                ExecuteAfterEditOptions {
1200                    segment_ids_edited: drag_anchor_segment_ids,
1201                    edit_kind: EditDeleteKind::Edit,
1202                    commit_solved_initial_guesses,
1203                },
1204            )
1205            .await?;
1206        if invalidates_ids {
1207            scene_graph_delta.invalidates_ids = true;
1208        }
1209        Ok((source_delta, scene_graph_delta))
1210    }
1211
1212    async fn delete_objects(
1213        &mut self,
1214        ctx: &ExecutorContext,
1215        _version: Version,
1216        sketch: ObjectId,
1217        constraint_ids: Vec<ObjectId>,
1218        segment_ids: Vec<ObjectId>,
1219    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1220        // TODO: Check version.
1221        let sketch_block_ref =
1222            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1223
1224        // Deduplicate IDs.
1225        let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1226        let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
1227
1228        // If a point is owned by a Line/Arc, we want to delete the owner, which will
1229        // also delete the point, as well as other points that are owned by the owner.
1230        let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
1231
1232        for segment_id in segment_ids_set.iter().copied() {
1233            let owner_id = self.scene_graph.objects.get(segment_id.0).and_then(|segment_object| {
1234                let ObjectKind::Segment { segment } = &segment_object.kind else {
1235                    return None;
1236                };
1237                match segment {
1238                    Segment::Point(point) => point.owner,
1239                    Segment::Line(line) => line.owner,
1240                    _ => None,
1241                }
1242            });
1243
1244            if let Some(owner_id) = owner_id
1245                && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
1246                && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
1247                && matches!(
1248                    owner_segment,
1249                    Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) | Segment::ControlPointSpline(_)
1250                )
1251            {
1252                // segment is owned -> delete the owner
1253                resolved_segment_ids_to_delete.insert(owner_id);
1254            } else {
1255                // segment is not owned by anything -> can be deleted
1256                resolved_segment_ids_to_delete.insert(segment_id);
1257            }
1258        }
1259        let referenced_constraint_ids = self
1260            .find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)
1261            .map_err(KclErrorWithOutputs::no_outputs)?;
1262
1263        let mut new_ast = self.program.ast.clone();
1264
1265        for constraint_id in referenced_constraint_ids {
1266            if constraint_ids_set.contains(&constraint_id) {
1267                continue;
1268            }
1269
1270            let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1271                KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Constraint not found: {constraint_id:?}")))
1272            })?;
1273            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
1274                return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1275                    "Object is not a constraint, it is {}",
1276                    constraint_object.kind.human_friendly_kind_with_article()
1277                ))));
1278            };
1279
1280            match constraint {
1281                Constraint::Coincident(coincident) => {
1282                    let remaining_segments =
1283                        self.remaining_constraint_segments(&coincident.segments, &resolved_segment_ids_to_delete);
1284
1285                    // If there are at least 2 segments left in the constraint: keep it, otherwise delete it.
1286                    if remaining_segments.len() >= 2 {
1287                        self.edit_coincident_constraint(&mut new_ast, constraint_id, remaining_segments)
1288                            .map_err(KclErrorWithOutputs::no_outputs)?;
1289                    } else {
1290                        constraint_ids_set.insert(constraint_id);
1291                    }
1292                }
1293                Constraint::EqualRadius(equal_radius) => {
1294                    let remaining_input = equal_radius
1295                        .input
1296                        .iter()
1297                        .copied()
1298                        .filter(|segment_id| {
1299                            !self.segment_will_be_deleted(*segment_id, &resolved_segment_ids_to_delete)
1300                        })
1301                        .collect::<Vec<_>>();
1302
1303                    if remaining_input.len() >= 2 {
1304                        self.edit_equal_radius_constraint(&mut new_ast, constraint_id, remaining_input)
1305                            .map_err(KclErrorWithOutputs::no_outputs)?;
1306                    } else {
1307                        constraint_ids_set.insert(constraint_id);
1308                    }
1309                }
1310                Constraint::LinesEqualLength(lines_equal_length) => {
1311                    let remaining_lines = lines_equal_length
1312                        .lines
1313                        .iter()
1314                        .copied()
1315                        .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1316                        .collect::<Vec<_>>();
1317
1318                    // Equal length constraint is only valid with at least 2 lines
1319                    if remaining_lines.len() >= 2 {
1320                        self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)
1321                            .map_err(KclErrorWithOutputs::no_outputs)?;
1322                    } else {
1323                        constraint_ids_set.insert(constraint_id);
1324                    }
1325                }
1326                Constraint::Parallel(parallel) => {
1327                    let remaining_lines = parallel
1328                        .lines
1329                        .iter()
1330                        .copied()
1331                        .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1332                        .collect::<Vec<_>>();
1333
1334                    if remaining_lines.len() >= 2 {
1335                        self.edit_parallel_constraint(&mut new_ast, constraint_id, remaining_lines)
1336                            .map_err(KclErrorWithOutputs::no_outputs)?;
1337                    } else {
1338                        constraint_ids_set.insert(constraint_id);
1339                    }
1340                }
1341                Constraint::Horizontal(Horizontal::Points { points }) => {
1342                    let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1343
1344                    if remaining_points.len() >= 2 {
1345                        self.edit_horizontal_points_constraint(&mut new_ast, constraint_id, remaining_points)
1346                            .map_err(KclErrorWithOutputs::no_outputs)?;
1347                    } else {
1348                        constraint_ids_set.insert(constraint_id);
1349                    }
1350                }
1351                Constraint::Vertical(Vertical::Points { points }) => {
1352                    let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1353
1354                    if remaining_points.len() >= 2 {
1355                        self.edit_vertical_points_constraint(&mut new_ast, constraint_id, remaining_points)
1356                            .map_err(KclErrorWithOutputs::no_outputs)?;
1357                    } else {
1358                        constraint_ids_set.insert(constraint_id);
1359                    }
1360                }
1361                Constraint::Fixed(fixed) => {
1362                    if fixed.points.iter().any(|fixed_point| {
1363                        self.segment_will_be_deleted(fixed_point.point, &resolved_segment_ids_to_delete)
1364                    }) {
1365                        constraint_ids_set.insert(constraint_id);
1366                    }
1367                }
1368                _ => {
1369                    // All other constraint types: if referenced by a segment -> delete the constraint
1370                    constraint_ids_set.insert(constraint_id);
1371                }
1372            }
1373        }
1374
1375        for constraint_id in constraint_ids_set {
1376            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1377                .map_err(KclErrorWithOutputs::no_outputs)?;
1378        }
1379        for segment_id in resolved_segment_ids_to_delete {
1380            self.delete_segment(&mut new_ast, sketch, segment_id)
1381                .map_err(KclErrorWithOutputs::no_outputs)?;
1382        }
1383
1384        self.execute_after_edit(
1385            ctx,
1386            sketch,
1387            sketch_block_ref,
1388            &mut new_ast,
1389            ExecuteAfterEditOptions {
1390                segment_ids_edited: Default::default(),
1391                edit_kind: EditDeleteKind::DeleteNonSketch,
1392                commit_solved_initial_guesses: true,
1393            },
1394        )
1395        .await
1396    }
1397
1398    async fn add_constraint(
1399        &mut self,
1400        ctx: &ExecutorContext,
1401        _version: Version,
1402        sketch: ObjectId,
1403        constraint: Constraint,
1404    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1405        // TODO: Check version.
1406
1407        // Save the original state as a backup - we'll restore it if anything fails
1408        let original_program = self.program.clone();
1409        let original_scene_graph = self.scene_graph.clone();
1410
1411        let mut new_ast = self.program.ast.clone();
1412        let sketch_block_ref = match constraint {
1413            Constraint::Coincident(coincident) => self
1414                .add_coincident(sketch, coincident, &mut new_ast)
1415                .await
1416                .map_err(KclErrorWithOutputs::no_outputs)?,
1417            Constraint::Distance(distance) => self
1418                .add_distance(sketch, distance, &mut new_ast)
1419                .await
1420                .map_err(KclErrorWithOutputs::no_outputs)?,
1421            Constraint::EqualRadius(equal_radius) => self
1422                .add_equal_radius(sketch, equal_radius, &mut new_ast)
1423                .await
1424                .map_err(KclErrorWithOutputs::no_outputs)?,
1425            Constraint::Fixed(fixed) => self
1426                .add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1427                .await
1428                .map_err(KclErrorWithOutputs::no_outputs)?,
1429            Constraint::HorizontalDistance(distance) => self
1430                .add_horizontal_distance(sketch, distance, &mut new_ast)
1431                .await
1432                .map_err(KclErrorWithOutputs::no_outputs)?,
1433            Constraint::VerticalDistance(distance) => self
1434                .add_vertical_distance(sketch, distance, &mut new_ast)
1435                .await
1436                .map_err(KclErrorWithOutputs::no_outputs)?,
1437            Constraint::Horizontal(horizontal) => self
1438                .add_horizontal(sketch, horizontal, &mut new_ast)
1439                .await
1440                .map_err(KclErrorWithOutputs::no_outputs)?,
1441            Constraint::LinesEqualLength(lines_equal_length) => self
1442                .add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1443                .await
1444                .map_err(KclErrorWithOutputs::no_outputs)?,
1445            Constraint::Midpoint(midpoint) => self
1446                .add_midpoint(sketch, midpoint, &mut new_ast)
1447                .await
1448                .map_err(KclErrorWithOutputs::no_outputs)?,
1449            Constraint::Parallel(parallel) => self
1450                .add_parallel(sketch, parallel, &mut new_ast)
1451                .await
1452                .map_err(KclErrorWithOutputs::no_outputs)?,
1453            Constraint::Perpendicular(perpendicular) => self
1454                .add_perpendicular(sketch, perpendicular, &mut new_ast)
1455                .await
1456                .map_err(KclErrorWithOutputs::no_outputs)?,
1457            Constraint::Radius(radius) => self
1458                .add_radius(sketch, radius, &mut new_ast)
1459                .await
1460                .map_err(KclErrorWithOutputs::no_outputs)?,
1461            Constraint::Diameter(diameter) => self
1462                .add_diameter(sketch, diameter, &mut new_ast)
1463                .await
1464                .map_err(KclErrorWithOutputs::no_outputs)?,
1465            Constraint::Symmetric(symmetric) => self
1466                .add_symmetric(sketch, symmetric, &mut new_ast)
1467                .await
1468                .map_err(KclErrorWithOutputs::no_outputs)?,
1469            Constraint::Vertical(vertical) => self
1470                .add_vertical(sketch, vertical, &mut new_ast)
1471                .await
1472                .map_err(KclErrorWithOutputs::no_outputs)?,
1473            Constraint::Angle(lines_at_angle) => self
1474                .add_angle(sketch, lines_at_angle, &mut new_ast)
1475                .await
1476                .map_err(KclErrorWithOutputs::no_outputs)?,
1477            Constraint::Tangent(tangent) => self
1478                .add_tangent(sketch, tangent, &mut new_ast)
1479                .await
1480                .map_err(KclErrorWithOutputs::no_outputs)?,
1481        };
1482
1483        let result = self
1484            .execute_after_add_constraint(ctx, sketch, sketch_block_ref, &mut new_ast)
1485            .await;
1486
1487        // If execution failed, restore the original state to prevent corruption
1488        if result.is_err() {
1489            self.program = original_program;
1490            self.scene_graph = original_scene_graph;
1491        }
1492
1493        result
1494    }
1495
1496    async fn chain_segment(
1497        &mut self,
1498        ctx: &ExecutorContext,
1499        version: Version,
1500        sketch: ObjectId,
1501        previous_segment_end_point_id: ObjectId,
1502        segment: SegmentCtor,
1503        _label: Option<String>,
1504    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1505        // TODO: Check version.
1506
1507        // First, add the segment (line) to get its start point ID
1508        let SegmentCtor::Line(line_ctor) = segment else {
1509            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1510                "chain_segment currently only supports Line segments, got {}",
1511                segment.human_friendly_kind_with_article(),
1512            ))));
1513        };
1514
1515        // Add the line segment first - this updates self.program and self.scene_graph
1516        let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
1517
1518        // Find the new line's start point ID from the updated scene graph
1519        // add_line updates self.scene_graph, so we can use that
1520        let new_line_id = first_scene_delta
1521            .new_objects
1522            .iter()
1523            .find(|&obj_id| {
1524                let obj = self.scene_graph.objects.get(obj_id.0);
1525                if let Some(obj) = obj {
1526                    matches!(
1527                        &obj.kind,
1528                        ObjectKind::Segment {
1529                            segment: Segment::Line(_)
1530                        }
1531                    )
1532                } else {
1533                    false
1534                }
1535            })
1536            .ok_or_else(|| {
1537                KclErrorWithOutputs::no_outputs(KclError::refactor(
1538                    "Failed to find new line segment in scene graph".to_string(),
1539                ))
1540            })?;
1541
1542        let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| {
1543            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1544                "New line object not found: {new_line_id:?}"
1545            )))
1546        })?;
1547
1548        let ObjectKind::Segment {
1549            segment: new_line_segment,
1550        } = &new_line_obj.kind
1551        else {
1552            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1553                "Object is not a segment: {new_line_obj:?}"
1554            ))));
1555        };
1556
1557        let Segment::Line(new_line) = new_line_segment else {
1558            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1559                "Segment is not a line: {new_line_segment:?}"
1560            ))));
1561        };
1562
1563        let new_line_start_point_id = new_line.start;
1564
1565        // Now add the coincident constraint between the previous end point and the new line's start point.
1566        let coincident = Coincident {
1567            segments: vec![previous_segment_end_point_id.into(), new_line_start_point_id.into()],
1568        };
1569
1570        let (final_src_delta, final_scene_delta) = self
1571            .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
1572            .await?;
1573
1574        // Combine new objects from the line addition and the constraint addition.
1575        // Both add_line and add_constraint now populate new_objects correctly.
1576        let mut combined_new_objects = first_scene_delta.new_objects.clone();
1577        combined_new_objects.extend(final_scene_delta.new_objects);
1578
1579        let scene_graph_delta = SceneGraphDelta {
1580            new_graph: self.scene_graph_for_ui(),
1581            invalidates_ids: false,
1582            new_objects: combined_new_objects,
1583            exec_outcome: final_scene_delta.exec_outcome,
1584        };
1585
1586        Ok((final_src_delta, scene_graph_delta))
1587    }
1588
1589    // Edit only the value, e.g. `distance(...) == 5mm` to `distance(...) == 7mm`.
1590    async fn edit_constraint_value(
1591        &mut self,
1592        ctx: &ExecutorContext,
1593        _version: Version,
1594        sketch: ObjectId,
1595        constraint_id: ObjectId,
1596        value_expression: String,
1597    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1598        // TODO: Check version.
1599        let sketch_block_ref =
1600            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1601
1602        let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1603            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1604        })?;
1605        if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
1606            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1607                "Object is not a constraint: {constraint_id:?}"
1608            ))));
1609        }
1610
1611        let mut new_ast = self.program.ast.clone();
1612
1613        // Parse the expression string into an AST node.
1614        let (parsed, errors) = Program::parse(&value_expression).map_err(|e| {
1615            KclErrorWithOutputs::no_outputs(KclError::refactor(format_kcl_error_message(
1616                "Invalid constraint value",
1617                &e,
1618            )))
1619        })?;
1620        if !errors.is_empty() {
1621            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1622                format_compilation_issues("Invalid constraint value", &errors),
1623            )));
1624        }
1625        let mut parsed = parsed.ok_or_else(|| {
1626            KclErrorWithOutputs::no_outputs(KclError::refactor("No AST produced from value expression".to_string()))
1627        })?;
1628        if parsed.ast.body.is_empty() {
1629            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1630                "Empty value expression".to_string(),
1631            )));
1632        }
1633        let first = parsed.ast.body.remove(0);
1634        let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
1635            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1636                "Value expression must be a simple expression".to_string(),
1637            )));
1638        };
1639
1640        let new_value: ast::BinaryPart = expr_stmt
1641            .inner
1642            .expression
1643            .try_into()
1644            .map_err(|e: String| KclErrorWithOutputs::no_outputs(KclError::refactor(e)))?;
1645
1646        self.mutate_ast(
1647            &mut new_ast,
1648            constraint_id,
1649            AstMutateCommand::EditConstraintValue { value: new_value },
1650        )
1651        .map_err(KclErrorWithOutputs::no_outputs)?;
1652
1653        self.execute_after_edit(
1654            ctx,
1655            sketch,
1656            sketch_block_ref,
1657            &mut new_ast,
1658            ExecuteAfterEditOptions {
1659                segment_ids_edited: Default::default(),
1660                edit_kind: EditDeleteKind::Edit,
1661                commit_solved_initial_guesses: true,
1662            },
1663        )
1664        .await
1665    }
1666
1667    async fn edit_distance_constraint_label_position(
1668        &mut self,
1669        ctx: &ExecutorContext,
1670        _version: Version,
1671        sketch: ObjectId,
1672        constraint_id: ObjectId,
1673        label_position: Point2d<Number>,
1674        anchor_segment_ids: Vec<ObjectId>,
1675    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1676        // TODO: Check version.
1677        let sketch_block_ref =
1678            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1679
1680        let mut new_ast = self.program.ast.clone();
1681        self.mutate_constraint_label_position(&mut new_ast, constraint_id, label_position)
1682            .map_err(KclErrorWithOutputs::no_outputs)?;
1683        let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
1684
1685        self.execute_after_edit(
1686            ctx,
1687            sketch,
1688            sketch_block_ref,
1689            &mut new_ast,
1690            ExecuteAfterEditOptions {
1691                segment_ids_edited: anchor_segment_ids.into_iter().collect(),
1692                edit_kind: EditDeleteKind::Edit,
1693                commit_solved_initial_guesses,
1694            },
1695        )
1696        .await
1697    }
1698
1699    /// Splitting a segment means creating a new segment, editing the old one, and then
1700    /// migrating a bunch of the constraints from the original segment to the new one
1701    /// (i.e. deleting them and re-adding them on the other segment).
1702    ///
1703    /// To keep this efficient we require as few executions as possible: we create the
1704    /// new segment first (to get its id), then do all edits and new constraints, and
1705    /// do all deletes at the end (since deletes invalidate ids).
1706    async fn batch_split_segment_operations(
1707        &mut self,
1708        ctx: &ExecutorContext,
1709        _version: Version,
1710        sketch: ObjectId,
1711        edit_segments: Vec<ExistingSegmentCtor>,
1712        add_constraints: Vec<Constraint>,
1713        delete_constraint_ids: Vec<ObjectId>,
1714        _new_segment_info: sketch::NewSegmentInfo,
1715    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1716        // TODO: Check version.
1717        let sketch_block_ref =
1718            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1719
1720        let mut new_ast = self.program.ast.clone();
1721        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1722
1723        // Step 1: Edit segments
1724        for segment in edit_segments {
1725            segment_ids_edited.insert(segment.id);
1726            match segment.ctor {
1727                SegmentCtor::Point(ctor) => self
1728                    .edit_point(&mut new_ast, sketch, segment.id, ctor)
1729                    .map_err(KclErrorWithOutputs::no_outputs)?,
1730                SegmentCtor::Line(ctor) => self
1731                    .edit_line(&mut new_ast, sketch, segment.id, ctor)
1732                    .map_err(KclErrorWithOutputs::no_outputs)?,
1733                SegmentCtor::Arc(ctor) => self
1734                    .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1735                    .map_err(KclErrorWithOutputs::no_outputs)?,
1736                SegmentCtor::Circle(ctor) => self
1737                    .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1738                    .map_err(KclErrorWithOutputs::no_outputs)?,
1739                SegmentCtor::ControlPointSpline(ctor) => self
1740                    .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1741                    .map_err(KclErrorWithOutputs::no_outputs)?,
1742            }
1743        }
1744
1745        // Step 2: Add all constraints
1746        for constraint in add_constraints {
1747            match constraint {
1748                Constraint::Coincident(coincident) => {
1749                    self.add_coincident(sketch, coincident, &mut new_ast)
1750                        .await
1751                        .map_err(KclErrorWithOutputs::no_outputs)?;
1752                }
1753                Constraint::Distance(distance) => {
1754                    self.add_distance(sketch, distance, &mut new_ast)
1755                        .await
1756                        .map_err(KclErrorWithOutputs::no_outputs)?;
1757                }
1758                Constraint::EqualRadius(equal_radius) => {
1759                    self.add_equal_radius(sketch, equal_radius, &mut new_ast)
1760                        .await
1761                        .map_err(KclErrorWithOutputs::no_outputs)?;
1762                }
1763                Constraint::Fixed(fixed) => {
1764                    self.add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1765                        .await
1766                        .map_err(KclErrorWithOutputs::no_outputs)?;
1767                }
1768                Constraint::HorizontalDistance(distance) => {
1769                    self.add_horizontal_distance(sketch, distance, &mut new_ast)
1770                        .await
1771                        .map_err(KclErrorWithOutputs::no_outputs)?;
1772                }
1773                Constraint::VerticalDistance(distance) => {
1774                    self.add_vertical_distance(sketch, distance, &mut new_ast)
1775                        .await
1776                        .map_err(KclErrorWithOutputs::no_outputs)?;
1777                }
1778                Constraint::Horizontal(horizontal) => {
1779                    self.add_horizontal(sketch, horizontal, &mut new_ast)
1780                        .await
1781                        .map_err(KclErrorWithOutputs::no_outputs)?;
1782                }
1783                Constraint::LinesEqualLength(lines_equal_length) => {
1784                    self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1785                        .await
1786                        .map_err(KclErrorWithOutputs::no_outputs)?;
1787                }
1788                Constraint::Midpoint(midpoint) => {
1789                    self.add_midpoint(sketch, midpoint, &mut new_ast)
1790                        .await
1791                        .map_err(KclErrorWithOutputs::no_outputs)?;
1792                }
1793                Constraint::Parallel(parallel) => {
1794                    self.add_parallel(sketch, parallel, &mut new_ast)
1795                        .await
1796                        .map_err(KclErrorWithOutputs::no_outputs)?;
1797                }
1798                Constraint::Perpendicular(perpendicular) => {
1799                    self.add_perpendicular(sketch, perpendicular, &mut new_ast)
1800                        .await
1801                        .map_err(KclErrorWithOutputs::no_outputs)?;
1802                }
1803                Constraint::Vertical(vertical) => {
1804                    self.add_vertical(sketch, vertical, &mut new_ast)
1805                        .await
1806                        .map_err(KclErrorWithOutputs::no_outputs)?;
1807                }
1808                Constraint::Diameter(diameter) => {
1809                    self.add_diameter(sketch, diameter, &mut new_ast)
1810                        .await
1811                        .map_err(KclErrorWithOutputs::no_outputs)?;
1812                }
1813                Constraint::Radius(radius) => {
1814                    self.add_radius(sketch, radius, &mut new_ast)
1815                        .await
1816                        .map_err(KclErrorWithOutputs::no_outputs)?;
1817                }
1818                Constraint::Symmetric(symmetric) => {
1819                    self.add_symmetric(sketch, symmetric, &mut new_ast)
1820                        .await
1821                        .map_err(KclErrorWithOutputs::no_outputs)?;
1822                }
1823                Constraint::Angle(angle) => {
1824                    self.add_angle(sketch, angle, &mut new_ast)
1825                        .await
1826                        .map_err(KclErrorWithOutputs::no_outputs)?;
1827                }
1828                Constraint::Tangent(tangent) => {
1829                    self.add_tangent(sketch, tangent, &mut new_ast)
1830                        .await
1831                        .map_err(KclErrorWithOutputs::no_outputs)?;
1832                }
1833            }
1834        }
1835
1836        // Step 3: Delete constraints (must be last since deletes can invalidate IDs)
1837        let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1838
1839        let has_constraint_deletions = !constraint_ids_set.is_empty();
1840        for constraint_id in constraint_ids_set {
1841            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1842                .map_err(KclErrorWithOutputs::no_outputs)?;
1843        }
1844
1845        // Step 4: Execute once at the end
1846        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
1847        // But we'll manually set invalidates_ids: true if we deleted constraints
1848        let (source_delta, mut scene_graph_delta) = self
1849            .execute_after_edit(
1850                ctx,
1851                sketch,
1852                sketch_block_ref,
1853                &mut new_ast,
1854                ExecuteAfterEditOptions {
1855                    segment_ids_edited,
1856                    edit_kind: EditDeleteKind::Edit,
1857                    commit_solved_initial_guesses: true,
1858                },
1859            )
1860            .await?;
1861
1862        // If we deleted constraints, set invalidates_ids: true
1863        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
1864        if has_constraint_deletions {
1865            scene_graph_delta.invalidates_ids = true;
1866        }
1867
1868        Ok((source_delta, scene_graph_delta))
1869    }
1870
1871    async fn batch_tail_cut_operations(
1872        &mut self,
1873        ctx: &ExecutorContext,
1874        _version: Version,
1875        sketch: ObjectId,
1876        edit_segments: Vec<ExistingSegmentCtor>,
1877        add_constraints: Vec<Constraint>,
1878        delete_constraint_ids: Vec<ObjectId>,
1879        additional_edited_segment_ids: Vec<ObjectId>,
1880    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1881        let sketch_block_ref =
1882            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1883
1884        let mut new_ast = self.program.ast.clone();
1885        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1886
1887        // Step 1: Edit segments (usually a single segment for tail cut)
1888        for segment in edit_segments {
1889            segment_ids_edited.insert(segment.id);
1890            match segment.ctor {
1891                SegmentCtor::Point(ctor) => self
1892                    .edit_point(&mut new_ast, sketch, segment.id, ctor)
1893                    .map_err(KclErrorWithOutputs::no_outputs)?,
1894                SegmentCtor::Line(ctor) => self
1895                    .edit_line(&mut new_ast, sketch, segment.id, ctor)
1896                    .map_err(KclErrorWithOutputs::no_outputs)?,
1897                SegmentCtor::Arc(ctor) => self
1898                    .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1899                    .map_err(KclErrorWithOutputs::no_outputs)?,
1900                SegmentCtor::Circle(ctor) => self
1901                    .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1902                    .map_err(KclErrorWithOutputs::no_outputs)?,
1903                SegmentCtor::ControlPointSpline(ctor) => self
1904                    .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1905                    .map_err(KclErrorWithOutputs::no_outputs)?,
1906            }
1907        }
1908
1909        segment_ids_edited.extend(additional_edited_segment_ids);
1910
1911        // Step 2: Add coincident constraints
1912        for constraint in add_constraints {
1913            match constraint {
1914                Constraint::Coincident(coincident) => {
1915                    self.add_coincident(sketch, coincident, &mut new_ast)
1916                        .await
1917                        .map_err(KclErrorWithOutputs::no_outputs)?;
1918                }
1919                other => {
1920                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1921                        "unsupported constraint in tail cut batch: {other:?}"
1922                    ))));
1923                }
1924            }
1925        }
1926
1927        // Step 3: Delete constraints (if any)
1928        let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1929
1930        let has_constraint_deletions = !constraint_ids_set.is_empty();
1931        for constraint_id in constraint_ids_set {
1932            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1933                .map_err(KclErrorWithOutputs::no_outputs)?;
1934        }
1935
1936        // Step 4: Single execute_after_edit
1937        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
1938        // But we'll manually set invalidates_ids: true if we deleted constraints
1939        let (source_delta, mut scene_graph_delta) = self
1940            .execute_after_edit(
1941                ctx,
1942                sketch,
1943                sketch_block_ref,
1944                &mut new_ast,
1945                ExecuteAfterEditOptions {
1946                    segment_ids_edited,
1947                    edit_kind: EditDeleteKind::Edit,
1948                    commit_solved_initial_guesses: true,
1949                },
1950            )
1951            .await?;
1952
1953        // If we deleted constraints, set invalidates_ids: true
1954        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
1955        if has_constraint_deletions {
1956            scene_graph_delta.invalidates_ids = true;
1957        }
1958
1959        Ok((source_delta, scene_graph_delta))
1960    }
1961}
1962
1963impl FrontendState {
1964    pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1965        self.program = program.clone();
1966
1967        // Execute so that the objects are updated and available for the next
1968        // API call.
1969        // This always uses engine execution (not mock) so that things are cached.
1970        // Engine execution now runs freedom analysis automatically.
1971        // Keep existing checkpoints alive here. History may still reference
1972        // older committed sketch states across a direct-edit boundary, and a
1973        // checkpoint restore is a full state replacement anyway. We append a
1974        // fresh baseline checkpoint after the full execution below.
1975        // Clear the freedom cache since IDs might have changed after direct editing
1976        // and we're about to run freedom analysis which will repopulate it.
1977        self.point_freedom_cache.clear();
1978        match ctx.run_with_caching(program).await {
1979            Ok(outcome) => {
1980                let outcome = self.update_state_after_exec(outcome, true);
1981                let checkpoint_id = self
1982                    .create_sketch_checkpoint(outcome.clone())
1983                    .await
1984                    .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
1985                Ok(SetProgramOutcome::Success {
1986                    scene_graph: Box::new(self.scene_graph_for_ui()),
1987                    exec_outcome: Box::new(outcome),
1988                    checkpoint_id: Some(checkpoint_id),
1989                })
1990            }
1991            Err(mut err) => {
1992                // Don't return an error just because execution failed. Instead,
1993                // update state as much as possible.
1994                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1995                self.update_state_after_exec(outcome, true);
1996                err.scene_graph = Some(self.scene_graph_for_ui());
1997                Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
1998            }
1999        }
2000    }
2001
2002    /// Decorate engine execution such that our state is updated and the scene
2003    /// graph is added to the return.
2004    pub async fn engine_execute(
2005        &mut self,
2006        ctx: &ExecutorContext,
2007        program: Program,
2008    ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
2009        self.program = program.clone();
2010
2011        // Engine execution now runs freedom analysis automatically. Clear the
2012        // freedom cache since IDs might have changed after direct editing, and
2013        // we're about to run freedom analysis which will repopulate it.
2014        self.point_freedom_cache.clear();
2015        match ctx.run_with_caching(program).await {
2016            Ok(outcome) => {
2017                let outcome = self.update_state_after_exec(outcome, true);
2018                Ok(SceneGraphDelta {
2019                    new_graph: self.scene_graph_for_ui(),
2020                    exec_outcome: outcome,
2021                    // We don't know what the new objects are.
2022                    new_objects: Default::default(),
2023                    // We don't know if IDs were invalidated.
2024                    invalidates_ids: Default::default(),
2025                })
2026            }
2027            Err(mut err) => {
2028                // Update state as much as possible, even when there's an error.
2029                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
2030                self.update_state_after_exec(outcome, true);
2031                err.scene_graph = Some(self.scene_graph_for_ui());
2032                Err(err)
2033            }
2034        }
2035    }
2036
2037    fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
2038        if matches!(err.error, KclError::EngineHangup { .. }) {
2039            // It's not ideal to special-case this, but this error is very
2040            // common during development, and it causes confusing downstream
2041            // errors that have nothing to do with the actual problem.
2042            return Err(err);
2043        }
2044
2045        let KclErrorWithOutputs {
2046            error,
2047            mut non_fatal,
2048            variables,
2049            operations,
2050            artifact_graph,
2051            scene_objects,
2052            source_range_to_object,
2053            var_solutions,
2054            refactor_metadata,
2055            filenames,
2056            source_files,
2057            default_planes,
2058            ..
2059        } = err;
2060
2061        non_fatal.push(CompilationIssue::fatal(issue_source_range(&error), error.get_message()));
2062
2063        Ok(ExecOutcome {
2064            variables,
2065            filenames,
2066            operations,
2067            artifact_graph,
2068            scene_objects,
2069            source_range_to_object,
2070            var_solutions,
2071            refactor_metadata,
2072            issues: non_fatal,
2073            source_files,
2074            default_planes,
2075        })
2076    }
2077
2078    async fn add_point(
2079        &mut self,
2080        ctx: &ExecutorContext,
2081        sketch: ObjectId,
2082        ctor: PointCtor,
2083    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2084        // Create updated KCL source from args.
2085        let at_ast = to_ast_point2d(&ctor.position)
2086            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2087        let point_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2088            callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
2089            unlabeled: None,
2090            arguments: vec![ast::LabeledArg {
2091                label: Some(ast::Identifier::new(POINT_AT_PARAM)),
2092                arg: at_ast,
2093            }],
2094            digest: None,
2095            non_code_meta: Default::default(),
2096        })));
2097
2098        // Look up existing sketch.
2099        let sketch_id = sketch;
2100        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2101            #[cfg(target_arch = "wasm32")]
2102            web_sys::console::error_1(
2103                &format!(
2104                    "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
2105                    self.scene_graph.objects
2106                )
2107                .into(),
2108            );
2109            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2110        })?;
2111        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2112            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2113                "Object is not a sketch, it is {}",
2114                sketch_object.kind.human_friendly_kind_with_article(),
2115            ))));
2116        };
2117        // Add the point to the AST of the sketch block.
2118        let mut new_ast = self.program.ast.clone();
2119        let (sketch_block_ref, _) = self
2120            .mutate_ast(
2121                &mut new_ast,
2122                sketch_id,
2123                AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
2124            )
2125            .map_err(KclErrorWithOutputs::no_outputs)?;
2126        // Convert to string source to create real source ranges.
2127        let new_source = source_from_ast(&new_ast);
2128        // Parse the new KCL source.
2129        let new_program = parse_frontend_mutation_source(
2130            &new_source,
2131            "Error parsing KCL source after adding point",
2132            "No AST produced after adding point",
2133        )?;
2134
2135        let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2136            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2137                "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
2138            )))
2139        })?;
2140
2141        // Make sure to only set this if there are no errors.
2142        self.program = new_program.clone();
2143
2144        // Truncate after the sketch block for mock execution.
2145        let mut truncated_program = new_program;
2146        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2147            .map_err(KclErrorWithOutputs::no_outputs)?;
2148
2149        // Execute.
2150        let outcome = ctx
2151            .run_mock(
2152                &truncated_program,
2153                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2154            )
2155            .await?;
2156
2157        let new_object_ids = {
2158            let make_err =
2159                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2160            let segment_id = outcome
2161                .source_range_to_object
2162                .get(&point_node_ref.range)
2163                .copied()
2164                .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
2165            let segment_object = outcome
2166                .scene_objects
2167                .get(segment_id.0)
2168                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2169            let ObjectKind::Segment { segment } = &segment_object.kind else {
2170                return Err(make_err(format!(
2171                    "Object is not a segment, it is {}",
2172                    segment_object.kind.human_friendly_kind_with_article()
2173                )));
2174            };
2175            let Segment::Point(_) = segment else {
2176                return Err(make_err(format!(
2177                    "Segment is not a point, it is {}",
2178                    segment.human_friendly_kind_with_article()
2179                )));
2180            };
2181            vec![segment_id]
2182        };
2183        let src_delta = SourceDelta { text: new_source };
2184        // Uses .no_freedom_analysis() so freedom_analysis: false
2185        let outcome = self.update_state_after_exec(outcome, false);
2186        let scene_graph_delta = SceneGraphDelta {
2187            new_graph: self.scene_graph_for_ui(),
2188            invalidates_ids: false,
2189            new_objects: new_object_ids,
2190            exec_outcome: outcome,
2191        };
2192        Ok((src_delta, scene_graph_delta))
2193    }
2194
2195    async fn add_line(
2196        &mut self,
2197        ctx: &ExecutorContext,
2198        sketch: ObjectId,
2199        ctor: LineCtor,
2200    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2201        // Create updated KCL source from args.
2202        let start_ast = to_ast_point2d(&ctor.start)
2203            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2204        let end_ast = to_ast_point2d(&ctor.end)
2205            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2206        let mut arguments = vec![
2207            ast::LabeledArg {
2208                label: Some(ast::Identifier::new(LINE_START_PARAM)),
2209                arg: start_ast,
2210            },
2211            ast::LabeledArg {
2212                label: Some(ast::Identifier::new(LINE_END_PARAM)),
2213                arg: end_ast,
2214            },
2215        ];
2216        // Add construction kwarg if construction is Some(true)
2217        if ctor.construction == Some(true) {
2218            arguments.push(ast::LabeledArg {
2219                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2220                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2221                    value: ast::LiteralValue::Bool(true),
2222                    raw: "true".to_string(),
2223                    digest: None,
2224                }))),
2225            });
2226        }
2227        let line_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2228            callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
2229            unlabeled: None,
2230            arguments,
2231            digest: None,
2232            non_code_meta: Default::default(),
2233        })));
2234
2235        // Look up existing sketch.
2236        let sketch_id = sketch;
2237        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2238            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2239        })?;
2240        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2241            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2242                "Object is not a sketch, it is {}",
2243                sketch_object.kind.human_friendly_kind_with_article(),
2244            ))));
2245        };
2246        // Add the line to the AST of the sketch block.
2247        let mut new_ast = self.program.ast.clone();
2248        let (sketch_block_ref, _) = self
2249            .mutate_ast(
2250                &mut new_ast,
2251                sketch_id,
2252                AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
2253            )
2254            .map_err(KclErrorWithOutputs::no_outputs)?;
2255        // Convert to string source to create real source ranges.
2256        let new_source = source_from_ast(&new_ast);
2257        // Parse the new KCL source.
2258        let new_program = parse_frontend_mutation_source(
2259            &new_source,
2260            "Error parsing KCL source after adding line",
2261            "No AST produced after adding line",
2262        )?;
2263
2264        let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2265            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2266                "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
2267            )))
2268        })?;
2269
2270        // Make sure to only set this if there are no errors.
2271        self.program = new_program.clone();
2272
2273        // Truncate after the sketch block for mock execution.
2274        let mut truncated_program = new_program;
2275        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2276            .map_err(KclErrorWithOutputs::no_outputs)?;
2277
2278        // Execute.
2279        let outcome = ctx
2280            .run_mock(
2281                &truncated_program,
2282                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2283            )
2284            .await?;
2285
2286        let new_object_ids = {
2287            let make_err =
2288                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2289            let segment_id = outcome
2290                .source_range_to_object
2291                .get(&line_node_ref.range)
2292                .copied()
2293                .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
2294            let segment_object = outcome
2295                .scene_object_by_id(segment_id)
2296                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2297            let ObjectKind::Segment { segment } = &segment_object.kind else {
2298                return Err(make_err(format!(
2299                    "Object is not a segment, it is {}",
2300                    segment_object.kind.human_friendly_kind_with_article()
2301                )));
2302            };
2303            let Segment::Line(line) = segment else {
2304                return Err(make_err(format!(
2305                    "Segment is not a line, it is {}",
2306                    segment.human_friendly_kind_with_article()
2307                )));
2308            };
2309            vec![line.start, line.end, segment_id]
2310        };
2311        let src_delta = SourceDelta { text: new_source };
2312        // Uses .no_freedom_analysis() so freedom_analysis: false
2313        let outcome = self.update_state_after_exec(outcome, false);
2314        let scene_graph_delta = SceneGraphDelta {
2315            new_graph: self.scene_graph_for_ui(),
2316            invalidates_ids: false,
2317            new_objects: new_object_ids,
2318            exec_outcome: outcome,
2319        };
2320        Ok((src_delta, scene_graph_delta))
2321    }
2322
2323    async fn add_arc(
2324        &mut self,
2325        ctx: &ExecutorContext,
2326        sketch: ObjectId,
2327        ctor: ArcCtor,
2328    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2329        // Create updated KCL source from args.
2330        let start_ast = to_ast_point2d(&ctor.start)
2331            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2332        let end_ast = to_ast_point2d(&ctor.end)
2333            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2334        let center_ast = to_ast_point2d(&ctor.center)
2335            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2336        let mut arguments = vec![
2337            ast::LabeledArg {
2338                label: Some(ast::Identifier::new(ARC_START_PARAM)),
2339                arg: start_ast,
2340            },
2341            ast::LabeledArg {
2342                label: Some(ast::Identifier::new(ARC_END_PARAM)),
2343                arg: end_ast,
2344            },
2345            ast::LabeledArg {
2346                label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
2347                arg: center_ast,
2348            },
2349        ];
2350        // Add direction kwarg if it's clockwise, since counterclockwise is the
2351        // default.
2352        if ctor.direction == Some(ArcDirection::Cw) {
2353            arguments.push(ast::LabeledArg {
2354                label: Some(ast::Identifier::new(ARC_DIRECTION_PARAM)),
2355                arg: ast::Expr::Name(BoxNode::new(ast::Name::new(ARC_DIRECTION_CW_NAME))),
2356            });
2357        }
2358        // Add construction kwarg if construction is Some(true)
2359        if ctor.construction == Some(true) {
2360            arguments.push(ast::LabeledArg {
2361                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2362                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2363                    value: ast::LiteralValue::Bool(true),
2364                    raw: "true".to_string(),
2365                    digest: None,
2366                }))),
2367            });
2368        }
2369        let arc_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2370            callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
2371            unlabeled: None,
2372            arguments,
2373            digest: None,
2374            non_code_meta: Default::default(),
2375        })));
2376
2377        // Look up existing sketch.
2378        let sketch_id = sketch;
2379        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2380            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2381        })?;
2382        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2383            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2384                "Object is not a sketch, it is {}",
2385                sketch_object.kind.human_friendly_kind_with_article(),
2386            ))));
2387        };
2388        // Add the arc to the AST of the sketch block.
2389        let mut new_ast = self.program.ast.clone();
2390        let (sketch_block_ref, _) = self
2391            .mutate_ast(
2392                &mut new_ast,
2393                sketch_id,
2394                AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
2395            )
2396            .map_err(KclErrorWithOutputs::no_outputs)?;
2397        // Convert to string source to create real source ranges.
2398        let new_source = source_from_ast(&new_ast);
2399        // Parse the new KCL source.
2400        let new_program = parse_frontend_mutation_source(
2401            &new_source,
2402            "Error parsing KCL source after adding arc",
2403            "No AST produced after adding arc",
2404        )?;
2405
2406        let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2407            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2408                "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
2409            )))
2410        })?;
2411
2412        // Make sure to only set this if there are no errors.
2413        self.program = new_program.clone();
2414
2415        // Truncate after the sketch block for mock execution.
2416        let mut truncated_program = new_program;
2417        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2418            .map_err(KclErrorWithOutputs::no_outputs)?;
2419
2420        // Execute.
2421        let outcome = ctx
2422            .run_mock(
2423                &truncated_program,
2424                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2425            )
2426            .await?;
2427
2428        let new_object_ids = {
2429            let make_err =
2430                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2431            let segment_id = outcome
2432                .source_range_to_object
2433                .get(&arc_node_ref.range)
2434                .copied()
2435                .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
2436            let segment_object = outcome
2437                .scene_objects
2438                .get(segment_id.0)
2439                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2440            let ObjectKind::Segment { segment } = &segment_object.kind else {
2441                return Err(make_err(format!(
2442                    "Object is not a segment, it is {}",
2443                    segment_object.kind.human_friendly_kind_with_article()
2444                )));
2445            };
2446            let Segment::Arc(arc) = segment else {
2447                return Err(make_err(format!(
2448                    "Segment is not an arc, it is {}",
2449                    segment.human_friendly_kind_with_article()
2450                )));
2451            };
2452            vec![arc.start, arc.end, arc.center, segment_id]
2453        };
2454        let src_delta = SourceDelta { text: new_source };
2455        // Uses .no_freedom_analysis() so freedom_analysis: false
2456        let outcome = self.update_state_after_exec(outcome, false);
2457        let scene_graph_delta = SceneGraphDelta {
2458            new_graph: self.scene_graph_for_ui(),
2459            invalidates_ids: false,
2460            new_objects: new_object_ids,
2461            exec_outcome: outcome,
2462        };
2463        Ok((src_delta, scene_graph_delta))
2464    }
2465
2466    async fn add_circle(
2467        &mut self,
2468        ctx: &ExecutorContext,
2469        sketch: ObjectId,
2470        ctor: CircleCtor,
2471    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2472        // Create updated KCL source from args.
2473        let start_ast = to_ast_point2d(&ctor.start)
2474            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2475        let center_ast = to_ast_point2d(&ctor.center)
2476            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2477        let mut arguments = vec![
2478            ast::LabeledArg {
2479                label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2480                arg: start_ast,
2481            },
2482            ast::LabeledArg {
2483                label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2484                arg: center_ast,
2485            },
2486        ];
2487        // Add construction kwarg if construction is Some(true)
2488        if ctor.construction == Some(true) {
2489            arguments.push(ast::LabeledArg {
2490                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2491                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2492                    value: ast::LiteralValue::Bool(true),
2493                    raw: "true".to_string(),
2494                    digest: None,
2495                }))),
2496            });
2497        }
2498        let circle_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2499            callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2500            unlabeled: None,
2501            arguments,
2502            digest: None,
2503            non_code_meta: Default::default(),
2504        })));
2505
2506        // Look up existing sketch.
2507        let sketch_id = sketch;
2508        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2509            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2510        })?;
2511        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2512            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2513                "Object is not a sketch, it is {}",
2514                sketch_object.kind.human_friendly_kind_with_article(),
2515            ))));
2516        };
2517        // Add the circle to the AST of the sketch block.
2518        let mut new_ast = self.program.ast.clone();
2519        let (sketch_block_ref, _) = self
2520            .mutate_ast(
2521                &mut new_ast,
2522                sketch_id,
2523                AstMutateCommand::AddSketchBlockVarDecl {
2524                    prefix: CIRCLE_VARIABLE.to_owned(),
2525                    expr: circle_ast,
2526                },
2527            )
2528            .map_err(KclErrorWithOutputs::no_outputs)?;
2529        // Convert to string source to create real source ranges.
2530        let new_source = source_from_ast(&new_ast);
2531        // Parse the new KCL source.
2532        let new_program = parse_frontend_mutation_source(
2533            &new_source,
2534            "Error parsing KCL source after adding circle",
2535            "No AST produced after adding circle",
2536        )?;
2537
2538        let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2539            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2540                "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2541            )))
2542        })?;
2543
2544        // Make sure to only set this if there are no errors.
2545        self.program = new_program.clone();
2546
2547        // Truncate after the sketch block for mock execution.
2548        let mut truncated_program = new_program;
2549        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2550            .map_err(KclErrorWithOutputs::no_outputs)?;
2551
2552        // Execute.
2553        let outcome = ctx
2554            .run_mock(
2555                &truncated_program,
2556                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2557            )
2558            .await?;
2559
2560        let new_object_ids = {
2561            let make_err =
2562                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2563            let segment_id = outcome
2564                .source_range_to_object
2565                .get(&circle_node_ref.range)
2566                .copied()
2567                .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2568            let segment_object = outcome
2569                .scene_objects
2570                .get(segment_id.0)
2571                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2572            let ObjectKind::Segment { segment } = &segment_object.kind else {
2573                return Err(make_err(format!(
2574                    "Object is not a segment, it is {}",
2575                    segment_object.kind.human_friendly_kind_with_article()
2576                )));
2577            };
2578            let Segment::Circle(circle) = segment else {
2579                return Err(make_err(format!(
2580                    "Segment is not a circle, it is {}",
2581                    segment.human_friendly_kind_with_article()
2582                )));
2583            };
2584            vec![circle.start, circle.center, segment_id]
2585        };
2586        let src_delta = SourceDelta { text: new_source };
2587        // Uses .no_freedom_analysis() so freedom_analysis: false
2588        let outcome = self.update_state_after_exec(outcome, false);
2589        let scene_graph_delta = SceneGraphDelta {
2590            new_graph: self.scene_graph_for_ui(),
2591            invalidates_ids: false,
2592            new_objects: new_object_ids,
2593            exec_outcome: outcome,
2594        };
2595        Ok((src_delta, scene_graph_delta))
2596    }
2597
2598    async fn add_control_point_spline(
2599        &mut self,
2600        ctx: &ExecutorContext,
2601        sketch: ObjectId,
2602        ctor: ControlPointSplineCtor,
2603    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2604        let new_program = ensure_control_point_spline_experimental_features(&self.program)
2605            .map_err(KclErrorWithOutputs::no_outputs)?;
2606
2607        let points_ast = to_ast_point2d_array(&ctor.points)
2608            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2609        let mut arguments = vec![ast::LabeledArg {
2610            label: Some(ast::Identifier::new(CONTROL_POINT_SPLINE_POINTS_PARAM)),
2611            arg: points_ast,
2612        }];
2613        if ctor.construction == Some(true) {
2614            arguments.push(ast::LabeledArg {
2615                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2616                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2617                    value: ast::LiteralValue::Bool(true),
2618                    raw: "true".to_string(),
2619                    digest: None,
2620                }))),
2621            });
2622        }
2623        let spline_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2624            callee: ast::Node::no_src(ast_sketch2_name(CONTROL_POINT_SPLINE_FN)),
2625            unlabeled: None,
2626            arguments,
2627            digest: None,
2628            non_code_meta: Default::default(),
2629        })));
2630
2631        let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
2632            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2633        })?;
2634        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2635            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2636                "Object is not a sketch, it is {}",
2637                sketch_object.kind.human_friendly_kind_with_article(),
2638            ))));
2639        };
2640
2641        let mut new_ast = new_program.ast.clone();
2642        let (sketch_block_ref, _) = self
2643            .mutate_ast(
2644                &mut new_ast,
2645                sketch,
2646                AstMutateCommand::AddSketchBlockExprStmt { expr: spline_ast },
2647            )
2648            .map_err(KclErrorWithOutputs::no_outputs)?;
2649        let new_source = source_from_ast(&new_ast);
2650        let new_program = parse_frontend_mutation_source(
2651            &new_source,
2652            "Error parsing KCL source after adding controlPointSpline",
2653            "No AST produced after adding controlPointSpline",
2654        )?;
2655
2656        let spline_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2657            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2658                "Source range of controlPointSpline not found in sketch block: {sketch_block_ref:?}; {err:?}"
2659            )))
2660        })?;
2661
2662        self.program = new_program.clone();
2663
2664        let mut truncated_program = new_program;
2665        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2666            .map_err(KclErrorWithOutputs::no_outputs)?;
2667
2668        let outcome = ctx
2669            .run_mock(
2670                &truncated_program,
2671                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2672            )
2673            .await?;
2674
2675        let new_object_ids = {
2676            let make_err =
2677                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2678            let segment_id = outcome
2679                .source_range_to_object
2680                .get(&spline_node_ref.range)
2681                .copied()
2682                .ok_or_else(|| {
2683                    make_err(format!(
2684                        "Source range of controlPointSpline not found: {spline_node_ref:?}"
2685                    ))
2686                })?;
2687            let segment_object = outcome
2688                .scene_objects
2689                .get(segment_id.0)
2690                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2691            let ObjectKind::Segment { segment } = &segment_object.kind else {
2692                return Err(make_err(format!(
2693                    "Object is not a segment, it is {}",
2694                    segment_object.kind.human_friendly_kind_with_article()
2695                )));
2696            };
2697            let Segment::ControlPointSpline(spline) = segment else {
2698                return Err(make_err(format!(
2699                    "Segment is not a control point spline, it is {}",
2700                    segment.human_friendly_kind_with_article()
2701                )));
2702            };
2703
2704            let mut ids = outcome
2705                .scene_objects
2706                .iter()
2707                .filter_map(|obj| match &obj.kind {
2708                    ObjectKind::Segment {
2709                        segment: Segment::Line(line),
2710                    } if line.owner == Some(segment_id) => Some(obj.id),
2711                    _ => None,
2712                })
2713                .collect::<Vec<_>>();
2714            ids.extend(spline.controls.clone());
2715            ids.push(segment_id);
2716            ids
2717        };
2718        let src_delta = SourceDelta { text: new_source };
2719        let outcome = self.update_state_after_exec(outcome, false);
2720        let scene_graph_delta = SceneGraphDelta {
2721            new_graph: self.scene_graph_for_ui(),
2722            invalidates_ids: false,
2723            new_objects: new_object_ids,
2724            exec_outcome: outcome,
2725        };
2726        Ok((src_delta, scene_graph_delta))
2727    }
2728
2729    fn edit_point(
2730        &mut self,
2731        new_ast: &mut ast::Node<ast::Program>,
2732        sketch: ObjectId,
2733        point: ObjectId,
2734        ctor: PointCtor,
2735    ) -> Result<(), KclError> {
2736        // Create updated KCL source from args.
2737        let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2738
2739        // Look up existing sketch.
2740        let sketch_id = sketch;
2741        let sketch_object = self
2742            .scene_graph
2743            .objects
2744            .get(sketch_id.0)
2745            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2746        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2747            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2748        };
2749        sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2750            KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2751        })?;
2752        // Look up existing point.
2753        let point_id = point;
2754        let point_object = self
2755            .scene_graph
2756            .objects
2757            .get(point_id.0)
2758            .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2759        let ObjectKind::Segment {
2760            segment: Segment::Point(point),
2761        } = &point_object.kind
2762        else {
2763            return Err(KclError::refactor(format!(
2764                "Object is not a point segment: {point_object:?}"
2765            )));
2766        };
2767
2768        // If the point is part of a line or arc, edit the line/arc instead.
2769        if let Some(owner_id) = point.owner {
2770            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2771                KclError::refactor(format!(
2772                    "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2773                ))
2774            })?;
2775            let ObjectKind::Segment { segment } = &owner_object.kind else {
2776                return Err(KclError::refactor(format!(
2777                    "Internal: Owner of point is not a segment, but found {}",
2778                    owner_object.kind.human_friendly_kind_with_article()
2779                )));
2780            };
2781
2782            // Handle Line owner
2783            if let Segment::Line(line) = segment {
2784                let SegmentCtor::Line(line_ctor) = &line.ctor else {
2785                    return Err(KclError::refactor(format!(
2786                        "Internal: Owner of point does not have line ctor, but found {}",
2787                        line.ctor.human_friendly_kind_with_article()
2788                    )));
2789                };
2790                let mut line_ctor = line_ctor.clone();
2791                // Which end of the line is this point?
2792                if line.start == point_id {
2793                    line_ctor.start = ctor.position;
2794                } else if line.end == point_id {
2795                    line_ctor.end = ctor.position;
2796                } else {
2797                    return Err(KclError::refactor(format!(
2798                        "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2799                    )));
2800                }
2801                return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2802            }
2803
2804            // Handle Arc owner
2805            if let Segment::Arc(arc) = segment {
2806                let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2807                    return Err(KclError::refactor(format!(
2808                        "Internal: Owner of point does not have arc ctor, but found {}",
2809                        arc.ctor.human_friendly_kind_with_article()
2810                    )));
2811                };
2812                let mut arc_ctor = arc_ctor.clone();
2813                // Which point of the arc is this? (center, start, or end)
2814                if arc.center == point_id {
2815                    arc_ctor.center = ctor.position;
2816                } else if arc.start == point_id {
2817                    arc_ctor.start = ctor.position;
2818                } else if arc.end == point_id {
2819                    arc_ctor.end = ctor.position;
2820                } else {
2821                    return Err(KclError::refactor(format!(
2822                        "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2823                    )));
2824                }
2825                return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2826            }
2827
2828            // Handle Circle owner
2829            if let Segment::Circle(circle) = segment {
2830                let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2831                    return Err(KclError::refactor(format!(
2832                        "Internal: Owner of point does not have circle ctor, but found {}",
2833                        circle.ctor.human_friendly_kind_with_article()
2834                    )));
2835                };
2836                let mut circle_ctor = circle_ctor.clone();
2837                if circle.center == point_id {
2838                    circle_ctor.center = ctor.position;
2839                } else if circle.start == point_id {
2840                    circle_ctor.start = ctor.position;
2841                } else {
2842                    return Err(KclError::refactor(format!(
2843                        "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2844                    )));
2845                }
2846                return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2847            }
2848
2849            if let Segment::ControlPointSpline(spline) = segment {
2850                let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor else {
2851                    return Err(KclError::refactor(format!(
2852                        "Internal: Owner of point does not have controlPointSpline ctor, but found {}",
2853                        spline.ctor.human_friendly_kind_with_article()
2854                    )));
2855                };
2856                let mut spline_ctor = spline_ctor.clone();
2857                let Some(control_index) = spline.controls.iter().position(|id| *id == point_id) else {
2858                    return Err(KclError::refactor(format!(
2859                        "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
2860                    )));
2861                };
2862                spline_ctor.points[control_index] = ctor.position;
2863                return self.edit_control_point_spline(new_ast, sketch_id, owner_id, spline_ctor);
2864            }
2865
2866            // If owner is neither Line, Arc, nor Circle, allow editing the point directly
2867            // (fall through to the point editing logic below)
2868        }
2869
2870        // Modify the point AST.
2871        self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2872        Ok(())
2873    }
2874
2875    fn edit_line(
2876        &mut self,
2877        new_ast: &mut ast::Node<ast::Program>,
2878        sketch: ObjectId,
2879        line: ObjectId,
2880        ctor: LineCtor,
2881    ) -> Result<(), KclError> {
2882        // Create updated KCL source from args.
2883        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2884        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2885
2886        // Look up existing sketch.
2887        let sketch_id = sketch;
2888        let sketch_object = self
2889            .scene_graph
2890            .objects
2891            .get(sketch_id.0)
2892            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2893        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2894            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2895        };
2896        sketch
2897            .segments
2898            .iter()
2899            .find(|o| **o == line)
2900            .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2901        // Look up existing line.
2902        let line_id = line;
2903        let line_object = self
2904            .scene_graph
2905            .objects
2906            .get(line_id.0)
2907            .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2908        let ObjectKind::Segment { .. } = &line_object.kind else {
2909            let kind = line_object.kind.human_friendly_kind_with_article();
2910            return Err(KclError::refactor(format!(
2911                "This constraint only works on Segments, but you selected {kind}"
2912            )));
2913        };
2914
2915        // Modify the line AST.
2916        self.mutate_ast(
2917            new_ast,
2918            line_id,
2919            AstMutateCommand::EditLine {
2920                start: new_start_ast,
2921                end: new_end_ast,
2922                construction: ctor.construction,
2923            },
2924        )?;
2925        Ok(())
2926    }
2927
2928    fn edit_arc(
2929        &mut self,
2930        new_ast: &mut ast::Node<ast::Program>,
2931        sketch: ObjectId,
2932        arc: ObjectId,
2933        ctor: ArcCtor,
2934    ) -> Result<(), KclError> {
2935        // Create updated KCL source from args.
2936        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2937        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2938        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2939
2940        // Look up existing sketch.
2941        let sketch_id = sketch;
2942        let sketch_object = self
2943            .scene_graph
2944            .objects
2945            .get(sketch_id.0)
2946            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2947        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2948            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2949        };
2950        sketch
2951            .segments
2952            .iter()
2953            .find(|o| **o == arc)
2954            .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2955        // Look up existing arc.
2956        let arc_id = arc;
2957        let arc_object = self
2958            .scene_graph
2959            .objects
2960            .get(arc_id.0)
2961            .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2962        let ObjectKind::Segment { .. } = &arc_object.kind else {
2963            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2964        };
2965
2966        // Modify the arc AST.
2967        self.mutate_ast(
2968            new_ast,
2969            arc_id,
2970            AstMutateCommand::EditArc {
2971                start: new_start_ast,
2972                end: new_end_ast,
2973                center: new_center_ast,
2974                direction: ctor.direction,
2975                construction: ctor.construction,
2976            },
2977        )?;
2978        Ok(())
2979    }
2980
2981    fn edit_circle(
2982        &mut self,
2983        new_ast: &mut ast::Node<ast::Program>,
2984        sketch: ObjectId,
2985        circle: ObjectId,
2986        ctor: CircleCtor,
2987    ) -> Result<(), KclError> {
2988        // Create updated KCL source from args.
2989        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2990        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2991
2992        // Look up existing sketch.
2993        let sketch_id = sketch;
2994        let sketch_object = self
2995            .scene_graph
2996            .objects
2997            .get(sketch_id.0)
2998            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2999        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3000            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3001        };
3002        sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
3003            KclError::refactor(format!(
3004                "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
3005            ))
3006        })?;
3007        // Look up existing circle.
3008        let circle_id = circle;
3009        let circle_object = self
3010            .scene_graph
3011            .objects
3012            .get(circle_id.0)
3013            .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
3014        let ObjectKind::Segment { .. } = &circle_object.kind else {
3015            return Err(KclError::refactor(format!(
3016                "Object is not a segment: {circle_object:?}"
3017            )));
3018        };
3019
3020        // Modify the circle AST.
3021        self.mutate_ast(
3022            new_ast,
3023            circle_id,
3024            AstMutateCommand::EditCircle {
3025                start: new_start_ast,
3026                center: new_center_ast,
3027                construction: ctor.construction,
3028            },
3029        )?;
3030        Ok(())
3031    }
3032
3033    fn edit_control_point_spline(
3034        &mut self,
3035        new_ast: &mut ast::Node<ast::Program>,
3036        sketch: ObjectId,
3037        spline: ObjectId,
3038        ctor: ControlPointSplineCtor,
3039    ) -> Result<(), KclError> {
3040        let points_ast = to_ast_point2d_array(&ctor.points).map_err(|err| KclError::refactor(err.to_string()))?;
3041
3042        let sketch_object = self
3043            .scene_graph
3044            .objects
3045            .get(sketch.0)
3046            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3047        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3048            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3049        };
3050        sketch.segments.iter().find(|o| **o == spline).ok_or_else(|| {
3051            KclError::refactor(format!(
3052                "Control point spline not found in sketch: spline={spline:?}, sketch={sketch:?}"
3053            ))
3054        })?;
3055
3056        let spline_object =
3057            self.scene_graph.objects.get(spline.0).ok_or_else(|| {
3058                KclError::refactor(format!("Control point spline not found in scene graph: {spline:?}"))
3059            })?;
3060        let ObjectKind::Segment { .. } = &spline_object.kind else {
3061            return Err(KclError::refactor(format!(
3062                "Object is not a segment: {spline_object:?}"
3063            )));
3064        };
3065
3066        self.mutate_ast(
3067            new_ast,
3068            spline,
3069            AstMutateCommand::EditControlPointSpline {
3070                points: points_ast,
3071                construction: ctor.construction,
3072            },
3073        )?;
3074        Ok(())
3075    }
3076
3077    fn delete_segment(
3078        &mut self,
3079        new_ast: &mut ast::Node<ast::Program>,
3080        sketch: ObjectId,
3081        segment_id: ObjectId,
3082    ) -> Result<(), KclError> {
3083        // Look up existing sketch.
3084        let sketch_id = sketch;
3085        let sketch_object = self
3086            .scene_graph
3087            .objects
3088            .get(sketch_id.0)
3089            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3090        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3091            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3092        };
3093        sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
3094            KclError::refactor(format!(
3095                "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
3096            ))
3097        })?;
3098        // Look up existing segment.
3099        let segment_object =
3100            self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
3101                KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
3102            })?;
3103        let ObjectKind::Segment { .. } = &segment_object.kind else {
3104            return Err(KclError::refactor(format!(
3105                "Object is not a segment, it is {}",
3106                segment_object.kind.human_friendly_kind_with_article()
3107            )));
3108        };
3109
3110        // Modify the AST to remove the segment.
3111        self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
3112        Ok(())
3113    }
3114
3115    fn delete_constraint(
3116        &mut self,
3117        new_ast: &mut ast::Node<ast::Program>,
3118        sketch: ObjectId,
3119        constraint_id: ObjectId,
3120    ) -> Result<(), KclError> {
3121        // Look up existing sketch.
3122        let sketch_id = sketch;
3123        let sketch_object = self
3124            .scene_graph
3125            .objects
3126            .get(sketch_id.0)
3127            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3128        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3129            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3130        };
3131        sketch
3132            .constraints
3133            .iter()
3134            .find(|o| **o == constraint_id)
3135            .ok_or_else(|| {
3136                KclError::refactor(format!(
3137                    "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
3138                ))
3139            })?;
3140        // Look up existing constraint.
3141        let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
3142            KclError::refactor(format!(
3143                "Constraint not found in scene graph: constraint={constraint_id:?}"
3144            ))
3145        })?;
3146        let ObjectKind::Constraint { .. } = &constraint_object.kind else {
3147            return Err(KclError::refactor(format!(
3148                "Object is not a constraint, it is {}",
3149                constraint_object.kind.human_friendly_kind_with_article()
3150            )));
3151        };
3152
3153        // Modify the AST to remove the constraint.
3154        self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
3155        Ok(())
3156    }
3157
3158    fn edit_coincident_constraint(
3159        &mut self,
3160        new_ast: &mut ast::Node<ast::Program>,
3161        constraint_id: ObjectId,
3162        segments: Vec<ConstraintSegment>,
3163    ) -> Result<(), KclError> {
3164        if segments.len() < 2 {
3165            return Err(KclError::refactor(format!(
3166                "Coincident constraint must have at least 2 inputs, got {}",
3167                segments.len()
3168            )));
3169        }
3170
3171        let segment_asts = segments
3172            .iter()
3173            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3174            .collect::<Result<Vec<_>, _>>()?;
3175
3176        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3177            elements: segment_asts,
3178            digest: None,
3179            non_code_meta: Default::default(),
3180        })));
3181
3182        self.mutate_ast(
3183            new_ast,
3184            constraint_id,
3185            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3186        )?;
3187        Ok(())
3188    }
3189
3190    fn edit_horizontal_points_constraint(
3191        &mut self,
3192        new_ast: &mut ast::Node<ast::Program>,
3193        constraint_id: ObjectId,
3194        points: Vec<ConstraintSegment>,
3195    ) -> Result<(), KclError> {
3196        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
3197    }
3198
3199    fn edit_vertical_points_constraint(
3200        &mut self,
3201        new_ast: &mut ast::Node<ast::Program>,
3202        constraint_id: ObjectId,
3203        points: Vec<ConstraintSegment>,
3204    ) -> Result<(), KclError> {
3205        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
3206    }
3207
3208    fn edit_axis_points_constraint(
3209        &mut self,
3210        new_ast: &mut ast::Node<ast::Program>,
3211        constraint_id: ObjectId,
3212        points: Vec<ConstraintSegment>,
3213        constraint_name: &str,
3214    ) -> Result<(), KclError> {
3215        if points.len() < 2 {
3216            return Err(KclError::refactor(format!(
3217                "{constraint_name} points constraint must have at least 2 points, got {}",
3218                points.len()
3219            )));
3220        }
3221
3222        let point_asts = points
3223            .iter()
3224            .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3225            .collect::<Result<Vec<_>, _>>()?;
3226
3227        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3228            elements: point_asts,
3229            digest: None,
3230            non_code_meta: Default::default(),
3231        })));
3232
3233        self.mutate_ast(
3234            new_ast,
3235            constraint_id,
3236            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3237        )?;
3238        Ok(())
3239    }
3240
3241    /// updates the equalLength constraint with the given lines
3242    fn edit_equal_length_constraint(
3243        &mut self,
3244        new_ast: &mut ast::Node<ast::Program>,
3245        constraint_id: ObjectId,
3246        lines: Vec<ObjectId>,
3247    ) -> Result<(), KclError> {
3248        if lines.len() < 2 {
3249            return Err(KclError::refactor(format!(
3250                "Lines equal length constraint must have at least 2 lines, got {}",
3251                lines.len()
3252            )));
3253        }
3254
3255        let line_asts = lines
3256            .iter()
3257            .map(|line_id| {
3258                let line_object = self
3259                    .scene_graph
3260                    .objects
3261                    .get(line_id.0)
3262                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3263                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3264                    let kind = line_object.kind.human_friendly_kind_with_article();
3265                    return Err(KclError::refactor(format!(
3266                        "This constraint only works on Segments, but you selected {kind}"
3267                    )));
3268                };
3269                let Segment::Line(_) = line_segment else {
3270                    let kind = line_segment.human_friendly_kind_with_article();
3271                    return Err(KclError::refactor(format!(
3272                        "Only lines can be made equal length, but you selected {kind}"
3273                    )));
3274                };
3275
3276                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3277            })
3278            .collect::<Result<Vec<_>, _>>()?;
3279
3280        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3281            elements: line_asts,
3282            digest: None,
3283            non_code_meta: Default::default(),
3284        })));
3285
3286        self.mutate_ast(
3287            new_ast,
3288            constraint_id,
3289            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3290        )?;
3291        Ok(())
3292    }
3293
3294    /// Updates the parallel constraint with the given lines.
3295    fn edit_parallel_constraint(
3296        &mut self,
3297        new_ast: &mut ast::Node<ast::Program>,
3298        constraint_id: ObjectId,
3299        lines: Vec<ObjectId>,
3300    ) -> Result<(), KclError> {
3301        if lines.len() < 2 {
3302            return Err(KclError::refactor(format!(
3303                "Parallel constraint must have at least 2 lines, got {}",
3304                lines.len()
3305            )));
3306        }
3307
3308        let line_asts = lines
3309            .iter()
3310            .map(|line_id| {
3311                let line_object = self
3312                    .scene_graph
3313                    .objects
3314                    .get(line_id.0)
3315                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3316                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3317                    let kind = line_object.kind.human_friendly_kind_with_article();
3318                    return Err(KclError::refactor(format!(
3319                        "This constraint only works on Segments, but you selected {kind}"
3320                    )));
3321                };
3322                let Segment::Line(_) = line_segment else {
3323                    let kind = line_segment.human_friendly_kind_with_article();
3324                    return Err(KclError::refactor(format!(
3325                        "Only lines can be made parallel, but you selected {kind}"
3326                    )));
3327                };
3328
3329                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3330            })
3331            .collect::<Result<Vec<_>, _>>()?;
3332
3333        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3334            elements: line_asts,
3335            digest: None,
3336            non_code_meta: Default::default(),
3337        })));
3338
3339        self.mutate_ast(
3340            new_ast,
3341            constraint_id,
3342            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3343        )?;
3344        Ok(())
3345    }
3346
3347    /// Updates the equalRadius constraint with the given segments.
3348    fn edit_equal_radius_constraint(
3349        &mut self,
3350        new_ast: &mut ast::Node<ast::Program>,
3351        constraint_id: ObjectId,
3352        input: Vec<ObjectId>,
3353    ) -> Result<(), KclError> {
3354        if input.len() < 2 {
3355            return Err(KclError::refactor(format!(
3356                "equalRadius constraint must have at least 2 segments, got {}",
3357                input.len()
3358            )));
3359        }
3360
3361        let input_asts = input
3362            .iter()
3363            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3364            .collect::<Result<Vec<_>, _>>()?;
3365
3366        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3367            elements: input_asts,
3368            digest: None,
3369            non_code_meta: Default::default(),
3370        })));
3371
3372        self.mutate_ast(
3373            new_ast,
3374            constraint_id,
3375            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3376        )?;
3377        Ok(())
3378    }
3379
3380    async fn execute_after_edit(
3381        &mut self,
3382        ctx: &ExecutorContext,
3383        sketch: ObjectId,
3384        sketch_block_ref: AstNodeRef,
3385        new_ast: &mut ast::Node<ast::Program>,
3386        options: ExecuteAfterEditOptions,
3387    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3388        let ExecuteAfterEditOptions {
3389            segment_ids_edited,
3390            edit_kind,
3391            commit_solved_initial_guesses,
3392        } = options;
3393
3394        // Convert to string source to create real source ranges.
3395        let new_source = source_from_ast(new_ast);
3396        // Parse the new KCL source.
3397        let new_program = parse_frontend_mutation_source(
3398            &new_source,
3399            "Error parsing KCL source after editing",
3400            "No AST produced after editing",
3401        )?;
3402
3403        // Truncate after the sketch block for mock execution.
3404        let is_delete = edit_kind.is_delete();
3405        let truncated_program = {
3406            let mut truncated_program = new_program.clone();
3407            only_sketch_block(
3408                &mut truncated_program.ast,
3409                &sketch_block_ref,
3410                edit_kind.to_change_kind(),
3411            )
3412            .map_err(KclErrorWithOutputs::no_outputs)?;
3413            truncated_program
3414        };
3415
3416        // Execute.
3417        let drag_anchors = self.next_segment_drag_anchors.take().unwrap_or_default();
3418        let mock_config = MockConfig {
3419            sketch_block_id: Some(sketch),
3420            freedom_analysis: is_delete,
3421            segment_ids_edited: segment_ids_edited.clone(),
3422            drag_anchors,
3423            ..Default::default()
3424        };
3425        let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
3426
3427        // Only now, after execution has succeeded, update self.program.
3428        self.program = new_program;
3429
3430        // Uses freedom_analysis: is_delete
3431        let outcome = self.update_state_after_exec(outcome, is_delete);
3432
3433        let src_delta = if commit_solved_initial_guesses {
3434            self.commit_var_solutions_to_program(&outcome, "editing")?
3435        } else {
3436            SourceDelta { text: new_source }
3437        };
3438        let scene_graph_delta = SceneGraphDelta {
3439            new_graph: self.scene_graph_for_ui(),
3440            invalidates_ids: is_delete,
3441            new_objects: Vec::new(),
3442            exec_outcome: outcome,
3443        };
3444        Ok((src_delta, scene_graph_delta))
3445    }
3446
3447    async fn execute_after_delete_sketch(
3448        &mut self,
3449        ctx: &ExecutorContext,
3450        new_ast: &mut ast::Node<ast::Program>,
3451    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3452        // Convert to string source to create real source ranges.
3453        let new_source = source_from_ast(new_ast);
3454        // Parse the new KCL source.
3455        let new_program = parse_frontend_mutation_source(
3456            &new_source,
3457            "Error parsing KCL source after editing",
3458            "No AST produced after editing",
3459        )?;
3460
3461        // Make sure to only set this if there are no errors.
3462        self.program = new_program.clone();
3463
3464        // We deleted the entire sketch block. It doesn't make sense to truncate
3465        // and execute only the sketch block. We execute the whole program with
3466        // a real engine.
3467
3468        // Execute.
3469        let outcome = ctx.run_with_caching(new_program).await?;
3470        let freedom_analysis_ran = true;
3471
3472        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
3473
3474        let src_delta = SourceDelta { text: new_source };
3475        let scene_graph_delta = SceneGraphDelta {
3476            new_graph: self.scene_graph_for_ui(),
3477            invalidates_ids: true,
3478            new_objects: Vec::new(),
3479            exec_outcome: outcome,
3480        };
3481        Ok((src_delta, scene_graph_delta))
3482    }
3483
3484    /// Map a point object id into an AST reference expression for use in
3485    /// constraints. If the point is owned by a segment (line or arc), we
3486    /// reference the appropriate property on that segment (e.g. `line1.start`,
3487    /// `arc1.center`). Otherwise we reference the point directly.
3488    fn point_id_to_ast_reference(
3489        &self,
3490        point_id: ObjectId,
3491        new_ast: &mut ast::Node<ast::Program>,
3492    ) -> Result<ast::Expr, KclError> {
3493        let point_object = self
3494            .scene_graph
3495            .objects
3496            .get(point_id.0)
3497            .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
3498        let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
3499            return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
3500        };
3501        let Segment::Point(point) = point_segment else {
3502            return Err(KclError::refactor(format!(
3503                "Only points are currently supported: {point_object:?}"
3504            )));
3505        };
3506
3507        if let Some(owner_id) = point.owner {
3508            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3509                KclError::refactor(format!(
3510                    "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
3511                ))
3512            })?;
3513            let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3514                return Err(KclError::refactor(format!(
3515                    "Owner of point is not a segment, but found {}",
3516                    owner_object.kind.human_friendly_kind_with_article()
3517                )));
3518            };
3519
3520            match owner_segment {
3521                Segment::Line(line) => {
3522                    let property = if line.start == point_id {
3523                        LINE_PROPERTY_START
3524                    } else if line.end == point_id {
3525                        LINE_PROPERTY_END
3526                    } else {
3527                        return Err(KclError::refactor(format!(
3528                            "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
3529                        )));
3530                    };
3531                    get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
3532                }
3533                Segment::Arc(arc) => {
3534                    let property = if arc.start == point_id {
3535                        ARC_PROPERTY_START
3536                    } else if arc.end == point_id {
3537                        ARC_PROPERTY_END
3538                    } else if arc.center == point_id {
3539                        ARC_PROPERTY_CENTER
3540                    } else {
3541                        return Err(KclError::refactor(format!(
3542                            "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
3543                        )));
3544                    };
3545                    get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
3546                }
3547                Segment::Circle(circle) => {
3548                    let property = if circle.start == point_id {
3549                        CIRCLE_PROPERTY_START
3550                    } else if circle.center == point_id {
3551                        CIRCLE_PROPERTY_CENTER
3552                    } else {
3553                        return Err(KclError::refactor(format!(
3554                            "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
3555                        )));
3556                    };
3557                    get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
3558                }
3559                Segment::ControlPointSpline(spline) => {
3560                    let Some(index) = spline.controls.iter().position(|id| *id == point_id) else {
3561                        return Err(KclError::refactor(format!(
3562                            "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
3563                        )));
3564                    };
3565                    let owner_expr =
3566                        get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3567                    let controls_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_CONTROLS);
3568                    Ok(create_index_expression(controls_expr, index))
3569                }
3570                _ => Err(KclError::refactor(format!(
3571                    "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
3572                ))),
3573            }
3574        } else {
3575            // Standalone point.
3576            get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
3577        }
3578    }
3579
3580    fn line_id_to_ast_reference(
3581        &self,
3582        line_id: ObjectId,
3583        new_ast: &mut ast::Node<ast::Program>,
3584    ) -> Result<ast::Expr, KclError> {
3585        let line_object = self
3586            .scene_graph
3587            .objects
3588            .get(line_id.0)
3589            .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3590        let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3591            return Err(KclError::refactor(format!("Object is not a segment: {line_object:?}")));
3592        };
3593        let Segment::Line(line) = line_segment else {
3594            return Err(KclError::refactor(format!(
3595                "Only lines are currently supported: {line_object:?}"
3596            )));
3597        };
3598
3599        if let Some(owner_id) = line.owner {
3600            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3601                KclError::refactor(format!(
3602                    "Owner of line not found in scene graph: line={line_id:?}, owner={owner_id:?}"
3603                ))
3604            })?;
3605            let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3606                return Err(KclError::refactor(format!(
3607                    "Owner of line is not a segment, but found {}",
3608                    owner_object.kind.human_friendly_kind_with_article()
3609                )));
3610            };
3611
3612            match owner_segment {
3613                Segment::ControlPointSpline(spline) => {
3614                    let Some(index) = spline
3615                        .controls
3616                        .windows(2)
3617                        .position(|window| window[0] == line.start && window[1] == line.end)
3618                    else {
3619                        return Err(KclError::refactor(format!(
3620                            "Internal: Line is not part of owner's controlPointSpline segment: line={line_id:?}, spline={owner_id:?}"
3621                        )));
3622                    };
3623                    let owner_expr =
3624                        get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3625                    let edges_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_EDGES);
3626                    Ok(create_index_expression(edges_expr, index))
3627                }
3628                _ => Err(KclError::refactor(format!(
3629                    "Internal: Owner of line is not a supported segment type for constraints: {owner_segment:?}"
3630                ))),
3631            }
3632        } else {
3633            get_or_insert_ast_reference(new_ast, &line_object.source, "line", None)
3634        }
3635    }
3636
3637    fn coincident_segment_to_ast(
3638        &self,
3639        segment: &ConstraintSegment,
3640        new_ast: &mut ast::Node<ast::Program>,
3641    ) -> Result<ast::Expr, KclError> {
3642        match segment {
3643            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3644            ConstraintSegment::Segment(segment_id) => self.segment_id_to_constraint_ast_reference(*segment_id, new_ast),
3645        }
3646    }
3647
3648    fn segment_id_to_constraint_ast_reference(
3649        &self,
3650        segment_id: ObjectId,
3651        new_ast: &mut ast::Node<ast::Program>,
3652    ) -> Result<ast::Expr, KclError> {
3653        let segment_object = self
3654            .scene_graph
3655            .objects
3656            .get(segment_id.0)
3657            .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
3658        let ObjectKind::Segment { segment } = &segment_object.kind else {
3659            return Err(KclError::refactor(format!(
3660                "Object is not a segment, it is {}",
3661                segment_object.kind.human_friendly_kind_with_article()
3662            )));
3663        };
3664
3665        match segment {
3666            Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3667            Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
3668            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None),
3669            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3670            Segment::ControlPointSpline(_) => {
3671                get_or_insert_ast_reference(new_ast, &segment_object.source, CONTROL_POINT_SPLINE_FN, None)
3672            }
3673        }
3674    }
3675
3676    fn axis_constraint_segment_to_ast(
3677        &self,
3678        segment: &ConstraintSegment,
3679        new_ast: &mut ast::Node<ast::Program>,
3680    ) -> Result<ast::Expr, KclError> {
3681        match segment {
3682            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3683            ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3684        }
3685    }
3686
3687    async fn add_coincident(
3688        &mut self,
3689        sketch: ObjectId,
3690        coincident: Coincident,
3691        new_ast: &mut ast::Node<ast::Program>,
3692    ) -> Result<AstNodeRef, KclError> {
3693        let sketch_id = sketch;
3694        for segment in &coincident.segments {
3695            let ConstraintSegment::Segment(segment_id) = segment else {
3696                continue;
3697            };
3698            let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
3699                continue;
3700            };
3701            if matches!(
3702                segment_object.kind,
3703                ObjectKind::Segment {
3704                    segment: Segment::ControlPointSpline(_)
3705                }
3706            ) {
3707                return Err(KclError::refactor(
3708                    "Coincident with a full controlPointSpline is not supported yet. Constrain a control point or spline edge instead."
3709                        .to_owned(),
3710                ));
3711            }
3712        }
3713        let segment_asts = coincident
3714            .segments
3715            .iter()
3716            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3717            .collect::<Result<Vec<_>, _>>()?;
3718        if segment_asts.len() < 2 {
3719            return Err(KclError::refactor(format!(
3720                "Coincident constraint must have at least 2 inputs, got {}",
3721                segment_asts.len()
3722            )));
3723        }
3724
3725        // Create the coincident() call using shared helper.
3726        let coincident_ast = create_coincident_ast(segment_asts);
3727
3728        // Add the line to the AST of the sketch block.
3729        let (sketch_block_ref, _) = self.mutate_ast(
3730            new_ast,
3731            sketch_id,
3732            AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3733        )?;
3734        Ok(sketch_block_ref)
3735    }
3736
3737    async fn add_distance(
3738        &mut self,
3739        sketch: ObjectId,
3740        distance: Distance,
3741        new_ast: &mut ast::Node<ast::Program>,
3742    ) -> Result<AstNodeRef, KclError> {
3743        self.add_distance_constraint(sketch, DISTANCE_FN, distance, new_ast)
3744    }
3745
3746    fn distance_constraint_ast_parts(
3747        &self,
3748        function_name: &str,
3749        distance: &Distance,
3750        new_ast: &mut ast::Node<ast::Program>,
3751    ) -> Result<(ast::BinaryPart, ast::BinaryPart), KclError> {
3752        let [segment0_ast, segment1_ast] = match distance.segments.as_slice() {
3753            [pt0, pt1] => [
3754                self.coincident_segment_to_ast(pt0, new_ast)?,
3755                self.coincident_segment_to_ast(pt1, new_ast)?,
3756            ],
3757            _ => {
3758                return Err(KclError::refactor(format!(
3759                    "Distance constraint must have exactly 2 segments, got {}",
3760                    distance.segments.len()
3761                )));
3762            }
3763        };
3764
3765        let arguments = match &distance.label_position {
3766            Some(label_position) => vec![ast::LabeledArg {
3767                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3768                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3769            }],
3770            None => Default::default(),
3771        };
3772
3773        let call = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
3774            callee: ast::Node::no_src(ast_sketch2_name(function_name)),
3775            unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
3776                ast::ArrayExpression {
3777                    elements: vec![segment0_ast, segment1_ast],
3778                    digest: None,
3779                    non_code_meta: Default::default(),
3780                },
3781            )))),
3782            arguments,
3783            digest: None,
3784            non_code_meta: Default::default(),
3785        })));
3786        let value = ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3787            value: ast::LiteralValue::Number {
3788                value: distance.distance.value,
3789                suffix: distance.distance.units,
3790            },
3791            raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3792                KclError::refactor(format!(
3793                    "Could not format numeric suffix: {:?}",
3794                    distance.distance.units
3795                ))
3796            })?,
3797            digest: None,
3798        })));
3799
3800        Ok((call, value))
3801    }
3802
3803    fn add_distance_constraint(
3804        &mut self,
3805        sketch: ObjectId,
3806        function_name: &str,
3807        distance: Distance,
3808        new_ast: &mut ast::Node<ast::Program>,
3809    ) -> Result<AstNodeRef, KclError> {
3810        let (call, value) = self.distance_constraint_ast_parts(function_name, &distance, new_ast)?;
3811        let distance_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
3812            left: call,
3813            operator: ast::BinaryOperator::Eq,
3814            right: value,
3815            digest: None,
3816        })));
3817
3818        let (sketch_block_ref, _) = self.mutate_ast(
3819            new_ast,
3820            sketch,
3821            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3822        )?;
3823        Ok(sketch_block_ref)
3824    }
3825
3826    async fn add_angle(
3827        &mut self,
3828        sketch: ObjectId,
3829        angle: Angle,
3830        new_ast: &mut ast::Node<ast::Program>,
3831    ) -> Result<AstNodeRef, KclError> {
3832        let sketch_id = sketch;
3833        let (angle_call_ast, angle_value_ast) = self.angle_constraint_ast_parts(&angle, new_ast)?;
3834        let angle_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
3835            left: angle_call_ast,
3836            operator: ast::BinaryOperator::Eq,
3837            right: angle_value_ast,
3838            digest: None,
3839        })));
3840
3841        // Add the line to the AST of the sketch block.
3842        let (sketch_block_ref, _) = self.mutate_ast(
3843            new_ast,
3844            sketch_id,
3845            AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3846        )?;
3847        Ok(sketch_block_ref)
3848    }
3849
3850    fn angle_constraint_ast_parts(
3851        &self,
3852        angle: &Angle,
3853        new_ast: &mut ast::Node<ast::Program>,
3854    ) -> Result<(ast::BinaryPart, ast::BinaryPart), KclError> {
3855        let &[l0_id, l1_id] = angle.lines.as_slice() else {
3856            return Err(KclError::refactor(format!(
3857                "Angle constraint must have exactly 2 lines, got {}",
3858                angle.lines.len()
3859            )));
3860        };
3861
3862        let l0_ast = self.line_id_to_ast_reference(l0_id, new_ast)?;
3863        let l1_ast = self.line_id_to_ast_reference(l1_id, new_ast)?;
3864        let lines_ast = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3865            elements: vec![l0_ast, l1_ast],
3866            digest: None,
3867            non_code_meta: Default::default(),
3868        })));
3869
3870        if angle.inverse == Some(true) && angle.sector.is_none() {
3871            return Err(KclError::refactor("Angle inverse requires an angle sector".to_owned()));
3872        }
3873
3874        let uses_angle_dimension = angle.sector.is_some();
3875        let mut arguments = if uses_angle_dimension {
3876            vec![ast::LabeledArg {
3877                label: Some(ast::Identifier::new(ANGLE_LINES_PARAM)),
3878                arg: lines_ast.clone(),
3879            }]
3880        } else {
3881            Default::default()
3882        };
3883
3884        if let Some(sector) = angle.sector {
3885            arguments.push(ast::LabeledArg {
3886                label: Some(ast::Identifier::new(ANGLE_SECTOR_PARAM)),
3887                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3888                    value: ast::LiteralValue::Number {
3889                        value: f64::from(sector),
3890                        suffix: NumericSuffix::None,
3891                    },
3892                    raw: sector.to_string(),
3893                    digest: None,
3894                }))),
3895            });
3896        }
3897
3898        if angle.inverse == Some(true) {
3899            arguments.push(ast::LabeledArg {
3900                label: Some(ast::Identifier::new(ANGLE_INVERSE_PARAM)),
3901                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3902                    value: ast::LiteralValue::Bool(true),
3903                    raw: true.to_string(),
3904                    digest: None,
3905                }))),
3906            });
3907        }
3908
3909        if let Some(label_position) = &angle.label_position {
3910            arguments.push(ast::LabeledArg {
3911                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3912                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3913            });
3914        }
3915
3916        let call = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
3917            callee: ast::Node::no_src(ast_sketch2_name(if uses_angle_dimension {
3918                ANGLE_DIMENSION_FN
3919            } else {
3920                ANGLE_FN
3921            })),
3922            unlabeled: (!uses_angle_dimension).then_some(lines_ast),
3923            arguments,
3924            digest: None,
3925            non_code_meta: Default::default(),
3926        })));
3927        let value = ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3928            value: ast::LiteralValue::Number {
3929                value: angle.angle.value,
3930                suffix: angle.angle.units,
3931            },
3932            raw: format_number_literal(angle.angle.value, angle.angle.units, None)
3933                .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units)))?,
3934            digest: None,
3935        })));
3936
3937        Ok((call, value))
3938    }
3939
3940    async fn add_tangent(
3941        &mut self,
3942        sketch: ObjectId,
3943        tangent: Tangent,
3944        new_ast: &mut ast::Node<ast::Program>,
3945    ) -> Result<AstNodeRef, KclError> {
3946        let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3947            return Err(KclError::refactor(format!(
3948                "Tangent constraint must have exactly 2 segments, got {}",
3949                tangent.input.len()
3950            )));
3951        };
3952        let sketch_id = sketch;
3953
3954        let seg0_object = self
3955            .scene_graph
3956            .objects
3957            .get(seg0_id.0)
3958            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3959        let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3960            return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3961        };
3962        let seg0_ast = match seg0_segment {
3963            Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3964                self.segment_id_to_constraint_ast_reference(seg0_id, new_ast)?
3965            }
3966            _ => {
3967                return Err(KclError::refactor(format!(
3968                    "Tangent supports only line/arc/circle segments for now, got: {seg0_segment:?}"
3969                )));
3970            }
3971        };
3972
3973        let seg1_object = self
3974            .scene_graph
3975            .objects
3976            .get(seg1_id.0)
3977            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3978        let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3979            return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3980        };
3981        let seg1_ast = match seg1_segment {
3982            Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3983                self.segment_id_to_constraint_ast_reference(seg1_id, new_ast)?
3984            }
3985            _ => {
3986                return Err(KclError::refactor(format!(
3987                    "Tangent supports only line/arc/circle segments for now, got: {seg1_segment:?}"
3988                )));
3989            }
3990        };
3991
3992        let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
3993        let (sketch_block_ref, _) = self.mutate_ast(
3994            new_ast,
3995            sketch_id,
3996            AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
3997        )?;
3998        Ok(sketch_block_ref)
3999    }
4000
4001    async fn add_symmetric(
4002        &mut self,
4003        sketch: ObjectId,
4004        symmetric: Symmetric,
4005        new_ast: &mut ast::Node<ast::Program>,
4006    ) -> Result<AstNodeRef, KclError> {
4007        let &[input0_id, input1_id] = symmetric.input.as_slice() else {
4008            return Err(KclError::refactor(format!(
4009                "Symmetric constraint must have exactly 2 inputs, got {}",
4010                symmetric.input.len()
4011            )));
4012        };
4013        let sketch_id = sketch;
4014
4015        let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
4016        let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
4017        let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
4018
4019        let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
4020        let (sketch_block_ref, _) = self.mutate_ast(
4021            new_ast,
4022            sketch_id,
4023            AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
4024        )?;
4025        Ok(sketch_block_ref)
4026    }
4027
4028    async fn add_midpoint(
4029        &mut self,
4030        sketch: ObjectId,
4031        midpoint: Midpoint,
4032        new_ast: &mut ast::Node<ast::Program>,
4033    ) -> Result<AstNodeRef, KclError> {
4034        let sketch_id = sketch;
4035        let point_ast = self.axis_constraint_segment_to_ast(&midpoint.point, new_ast)?;
4036
4037        let segment_object = self
4038            .scene_graph
4039            .objects
4040            .get(midpoint.segment.0)
4041            .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
4042        let ObjectKind::Segment {
4043            segment: midpoint_segment,
4044        } = &segment_object.kind
4045        else {
4046            return Err(KclError::refactor(format!(
4047                "Object must be a segment, but it was {}",
4048                segment_object.kind.human_friendly_kind_with_article()
4049            )));
4050        };
4051        let segment_ast = match midpoint_segment {
4052            Segment::Line(_) => self.line_id_to_ast_reference(midpoint.segment, new_ast)?,
4053            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
4054            _ => {
4055                return Err(KclError::refactor(format!(
4056                    "Midpoint target must be a line or arc segment but it was {}",
4057                    midpoint_segment.human_friendly_kind_with_article()
4058                )));
4059            }
4060        };
4061
4062        let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
4063        let (sketch_block_ref, _) = self.mutate_ast(
4064            new_ast,
4065            sketch_id,
4066            AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
4067        )?;
4068        Ok(sketch_block_ref)
4069    }
4070
4071    async fn add_equal_radius(
4072        &mut self,
4073        sketch: ObjectId,
4074        equal_radius: EqualRadius,
4075        new_ast: &mut ast::Node<ast::Program>,
4076    ) -> Result<AstNodeRef, KclError> {
4077        if equal_radius.input.len() < 2 {
4078            return Err(KclError::refactor(format!(
4079                "equalRadius constraint must have at least 2 segments, got {}",
4080                equal_radius.input.len()
4081            )));
4082        }
4083
4084        let sketch_id = sketch;
4085        let input_asts = equal_radius
4086            .input
4087            .iter()
4088            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
4089            .collect::<Result<Vec<_>, _>>()?;
4090
4091        let equal_radius_ast = create_equal_radius_ast(input_asts);
4092        let (sketch_block_ref, _) = self.mutate_ast(
4093            new_ast,
4094            sketch_id,
4095            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
4096        )?;
4097        Ok(sketch_block_ref)
4098    }
4099
4100    async fn add_radius(
4101        &mut self,
4102        sketch: ObjectId,
4103        radius: Radius,
4104        new_ast: &mut ast::Node<ast::Program>,
4105    ) -> Result<AstNodeRef, KclError> {
4106        let params = ArcSizeConstraintParams {
4107            points: vec![radius.arc],
4108            function_name: RADIUS_FN,
4109            value: radius.radius.value,
4110            units: radius.radius.units,
4111            label_position: radius.label_position,
4112            constraint_type_name: "Radius",
4113        };
4114        self.add_arc_size_constraint(sketch, params, new_ast).await
4115    }
4116
4117    async fn add_diameter(
4118        &mut self,
4119        sketch: ObjectId,
4120        diameter: Diameter,
4121        new_ast: &mut ast::Node<ast::Program>,
4122    ) -> Result<AstNodeRef, KclError> {
4123        let params = ArcSizeConstraintParams {
4124            points: vec![diameter.arc],
4125            function_name: DIAMETER_FN,
4126            value: diameter.diameter.value,
4127            units: diameter.diameter.units,
4128            label_position: diameter.label_position,
4129            constraint_type_name: "Diameter",
4130        };
4131        self.add_arc_size_constraint(sketch, params, new_ast).await
4132    }
4133
4134    async fn add_fixed_constraints(
4135        &mut self,
4136        sketch: ObjectId,
4137        points: Vec<FixedPoint>,
4138        new_ast: &mut ast::Node<ast::Program>,
4139    ) -> Result<AstNodeRef, KclError> {
4140        let mut sketch_block_ref = None;
4141
4142        for fixed_point in points {
4143            let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
4144            let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
4145                .map_err(|err| KclError::refactor(err.to_string()))?;
4146
4147            let (sketch_ref, _) = self.mutate_ast(
4148                new_ast,
4149                sketch,
4150                AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
4151            )?;
4152            sketch_block_ref = Some(sketch_ref);
4153        }
4154
4155        sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
4156    }
4157
4158    async fn add_arc_size_constraint(
4159        &mut self,
4160        sketch: ObjectId,
4161        params: ArcSizeConstraintParams,
4162        new_ast: &mut ast::Node<ast::Program>,
4163    ) -> Result<AstNodeRef, KclError> {
4164        let sketch_id = sketch;
4165
4166        // Constraint must have exactly 1 argument (arc segment)
4167        if params.points.len() != 1 {
4168            return Err(KclError::refactor(format!(
4169                "{} constraint must have exactly 1 argument (an arc segment), got {}",
4170                params.constraint_type_name,
4171                params.points.len()
4172            )));
4173        }
4174
4175        let arc_id = params.points[0];
4176        let arc_object = self
4177            .scene_graph
4178            .objects
4179            .get(arc_id.0)
4180            .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
4181        let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
4182            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
4183        };
4184        let ref_type = match arc_segment {
4185            Segment::Arc(_) => ARC_VARIABLE,
4186            Segment::Circle(_) => CIRCLE_VARIABLE,
4187            _ => {
4188                return Err(KclError::refactor(format!(
4189                    "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
4190                    params.constraint_type_name
4191                )));
4192            }
4193        };
4194        // Reference the arc/circle segment directly
4195        let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
4196        let arguments = match &params.label_position {
4197            Some(label_position) => vec![ast::LabeledArg {
4198                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4199                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4200            }],
4201            None => Default::default(),
4202        };
4203
4204        // Create the function call.
4205        let call_ast = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4206            callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
4207            unlabeled: Some(arc_ast),
4208            arguments,
4209            digest: None,
4210            non_code_meta: Default::default(),
4211        })));
4212        let constraint_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
4213            left: call_ast,
4214            operator: ast::BinaryOperator::Eq,
4215            right: ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
4216                value: ast::LiteralValue::Number {
4217                    value: params.value,
4218                    suffix: params.units,
4219                },
4220                raw: format_number_literal(params.value, params.units, None)
4221                    .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
4222                digest: None,
4223            }))),
4224            digest: None,
4225        })));
4226
4227        // Add the line to the AST of the sketch block.
4228        let (sketch_block_ref, _) = self.mutate_ast(
4229            new_ast,
4230            sketch_id,
4231            AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
4232        )?;
4233        Ok(sketch_block_ref)
4234    }
4235
4236    async fn add_horizontal_distance(
4237        &mut self,
4238        sketch: ObjectId,
4239        distance: Distance,
4240        new_ast: &mut ast::Node<ast::Program>,
4241    ) -> Result<AstNodeRef, KclError> {
4242        self.add_distance_constraint(sketch, HORIZONTAL_DISTANCE_FN, distance, new_ast)
4243    }
4244
4245    async fn add_vertical_distance(
4246        &mut self,
4247        sketch: ObjectId,
4248        distance: Distance,
4249        new_ast: &mut ast::Node<ast::Program>,
4250    ) -> Result<AstNodeRef, KclError> {
4251        self.add_distance_constraint(sketch, VERTICAL_DISTANCE_FN, distance, new_ast)
4252    }
4253
4254    async fn add_horizontal(
4255        &mut self,
4256        sketch: ObjectId,
4257        horizontal: Horizontal,
4258        new_ast: &mut ast::Node<ast::Program>,
4259    ) -> Result<AstNodeRef, KclError> {
4260        let sketch_id = sketch;
4261
4262        // Map the runtime objects back to variable names.
4263        let first_arg_ast = match horizontal {
4264            Horizontal::Line { line } => {
4265                let line_object = self
4266                    .scene_graph
4267                    .objects
4268                    .get(line.0)
4269                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4270                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4271                    let kind = line_object.kind.human_friendly_kind_with_article();
4272                    return Err(KclError::refactor(format!(
4273                        "This constraint only works on Segments, but you selected {kind}"
4274                    )));
4275                };
4276                let Segment::Line(_) = line_segment else {
4277                    return Err(KclError::refactor(format!(
4278                        "Only lines can be made horizontal, but you selected {}",
4279                        line_segment.human_friendly_kind_with_article(),
4280                    )));
4281                };
4282                self.line_id_to_ast_reference(line, new_ast)?
4283            }
4284            Horizontal::Points { points } => {
4285                let point_asts = points
4286                    .iter()
4287                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4288                    .collect::<Result<Vec<_>, _>>()?;
4289                ast::ArrayExpression::new(point_asts).into()
4290            }
4291        };
4292        // Create the horizontal() call using shared helper.
4293        let horizontal_ast = create_horizontal_ast(first_arg_ast);
4294
4295        // Add the line to the AST of the sketch block.
4296        let (sketch_block_ref, _) = self.mutate_ast(
4297            new_ast,
4298            sketch_id,
4299            AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
4300        )?;
4301        Ok(sketch_block_ref)
4302    }
4303
4304    async fn add_lines_equal_length(
4305        &mut self,
4306        sketch: ObjectId,
4307        lines_equal_length: LinesEqualLength,
4308        new_ast: &mut ast::Node<ast::Program>,
4309    ) -> Result<AstNodeRef, KclError> {
4310        if lines_equal_length.lines.len() < 2 {
4311            return Err(KclError::refactor(format!(
4312                "Lines equal length constraint must have at least 2 lines, got {}",
4313                lines_equal_length.lines.len()
4314            )));
4315        };
4316
4317        let sketch_id = sketch;
4318
4319        // Map the runtime objects back to variable names.
4320        let line_asts = lines_equal_length
4321            .lines
4322            .iter()
4323            .map(|line_id| {
4324                let line_object = self
4325                    .scene_graph
4326                    .objects
4327                    .get(line_id.0)
4328                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4329                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4330                    let kind = line_object.kind.human_friendly_kind_with_article();
4331                    return Err(KclError::refactor(format!(
4332                        "This constraint only works on Segments, but you selected {kind}"
4333                    )));
4334                };
4335                let Segment::Line(_) = line_segment else {
4336                    let kind = line_segment.human_friendly_kind_with_article();
4337                    return Err(KclError::refactor(format!(
4338                        "Only lines can be made equal length, but you selected {kind}"
4339                    )));
4340                };
4341
4342                self.line_id_to_ast_reference(*line_id, new_ast)
4343            })
4344            .collect::<Result<Vec<_>, _>>()?;
4345
4346        // Create the equalLength() call using shared helper.
4347        let equal_length_ast = create_equal_length_ast(line_asts);
4348
4349        // Add the constraint to the AST of the sketch block.
4350        let (sketch_block_ref, _) = self.mutate_ast(
4351            new_ast,
4352            sketch_id,
4353            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
4354        )?;
4355        Ok(sketch_block_ref)
4356    }
4357
4358    fn equal_radius_segment_id_to_ast_reference(
4359        &mut self,
4360        segment_id: ObjectId,
4361        new_ast: &mut ast::Node<ast::Program>,
4362    ) -> Result<ast::Expr, KclError> {
4363        let segment_object = self
4364            .scene_graph
4365            .objects
4366            .get(segment_id.0)
4367            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4368        let ObjectKind::Segment { segment } = &segment_object.kind else {
4369            return Err(KclError::refactor(format!(
4370                "Object is not a segment, it was {}",
4371                segment_object.kind.human_friendly_kind_with_article()
4372            )));
4373        };
4374
4375        let ref_type = match segment {
4376            Segment::Arc(_) => ARC_VARIABLE,
4377            Segment::Circle(_) => CIRCLE_VARIABLE,
4378            _ => {
4379                return Err(KclError::refactor(format!(
4380                    "equalRadius supports only arc/circle segments, got {}",
4381                    segment.human_friendly_kind_with_article()
4382                )));
4383            }
4384        };
4385
4386        get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
4387    }
4388
4389    fn symmetric_input_id_to_ast_reference(
4390        &mut self,
4391        segment_id: ObjectId,
4392        new_ast: &mut ast::Node<ast::Program>,
4393    ) -> Result<ast::Expr, KclError> {
4394        let segment_object = self
4395            .scene_graph
4396            .objects
4397            .get(segment_id.0)
4398            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4399        let ObjectKind::Segment { segment } = &segment_object.kind else {
4400            return Err(KclError::refactor(format!(
4401                "Object is not a segment, it was {}",
4402                segment_object.kind.human_friendly_kind_with_article()
4403            )));
4404        };
4405
4406        match segment {
4407            Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
4408            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
4409            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
4410            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
4411            Segment::ControlPointSpline(_) => Err(KclError::refactor(
4412                "Symmetric does not yet support control point splines".to_owned(),
4413            )),
4414        }
4415    }
4416
4417    fn symmetric_axis_id_to_ast_reference(
4418        &mut self,
4419        segment_id: ObjectId,
4420        new_ast: &mut ast::Node<ast::Program>,
4421    ) -> Result<ast::Expr, KclError> {
4422        let segment_object = self
4423            .scene_graph
4424            .objects
4425            .get(segment_id.0)
4426            .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
4427        let ObjectKind::Segment { segment } = &segment_object.kind else {
4428            return Err(KclError::refactor(format!(
4429                "Object is not a segment, it was {}",
4430                segment_object.kind.human_friendly_kind_with_article()
4431            )));
4432        };
4433        match segment {
4434            Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
4435            _ => Err(KclError::refactor(format!(
4436                "Symmetric axis must be a line, got {}",
4437                segment.human_friendly_kind_with_article()
4438            ))),
4439        }
4440    }
4441
4442    async fn add_parallel(
4443        &mut self,
4444        sketch: ObjectId,
4445        parallel: Parallel,
4446        new_ast: &mut ast::Node<ast::Program>,
4447    ) -> Result<AstNodeRef, KclError> {
4448        if parallel.lines.len() < 2 {
4449            return Err(KclError::refactor(format!(
4450                "Parallel constraint must have at least 2 lines, got {}",
4451                parallel.lines.len()
4452            )));
4453        };
4454
4455        let sketch_id = sketch;
4456
4457        let line_asts = parallel
4458            .lines
4459            .iter()
4460            .map(|line_id| {
4461                let line_object = self
4462                    .scene_graph
4463                    .objects
4464                    .get(line_id.0)
4465                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4466                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4467                    let kind = line_object.kind.human_friendly_kind_with_article();
4468                    return Err(KclError::refactor(format!(
4469                        "This constraint only works on Segments, but you selected {kind}"
4470                    )));
4471                };
4472                let Segment::Line(_) = line_segment else {
4473                    let kind = line_segment.human_friendly_kind_with_article();
4474                    return Err(KclError::refactor(format!(
4475                        "Only lines can be made parallel, but you selected {kind}"
4476                    )));
4477                };
4478
4479                self.line_id_to_ast_reference(*line_id, new_ast)
4480            })
4481            .collect::<Result<Vec<_>, _>>()?;
4482
4483        let call_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4484            callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
4485            unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
4486                ast::ArrayExpression {
4487                    elements: line_asts,
4488                    digest: None,
4489                    non_code_meta: Default::default(),
4490                },
4491            )))),
4492            arguments: Default::default(),
4493            digest: None,
4494            non_code_meta: Default::default(),
4495        })));
4496
4497        let (sketch_block_ref, _) = self.mutate_ast(
4498            new_ast,
4499            sketch_id,
4500            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4501        )?;
4502        Ok(sketch_block_ref)
4503    }
4504
4505    async fn add_perpendicular(
4506        &mut self,
4507        sketch: ObjectId,
4508        perpendicular: Perpendicular,
4509        new_ast: &mut ast::Node<ast::Program>,
4510    ) -> Result<AstNodeRef, KclError> {
4511        self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
4512            .await
4513    }
4514
4515    async fn add_lines_at_angle_constraint(
4516        &mut self,
4517        sketch: ObjectId,
4518        angle_kind: LinesAtAngleKind,
4519        lines: Vec<ObjectId>,
4520        new_ast: &mut ast::Node<ast::Program>,
4521    ) -> Result<AstNodeRef, KclError> {
4522        let &[line0_id, line1_id] = lines.as_slice() else {
4523            return Err(KclError::refactor(format!(
4524                "{} constraint must have exactly 2 lines, got {}",
4525                angle_kind.to_function_name(),
4526                lines.len()
4527            )));
4528        };
4529
4530        let sketch_id = sketch;
4531
4532        // Map the runtime objects back to variable names.
4533        let line0_object = self
4534            .scene_graph
4535            .objects
4536            .get(line0_id.0)
4537            .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
4538        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
4539            let kind = line0_object.kind.human_friendly_kind_with_article();
4540            return Err(KclError::refactor(format!(
4541                "This constraint only works on Segments, but you selected {kind}"
4542            )));
4543        };
4544        let Segment::Line(_) = line0_segment else {
4545            return Err(KclError::refactor(format!(
4546                "Only lines can be made {}, but you selected {}",
4547                angle_kind.to_function_name(),
4548                line0_segment.human_friendly_kind_with_article(),
4549            )));
4550        };
4551        let line0_ast = self.line_id_to_ast_reference(line0_id, new_ast)?;
4552
4553        let line1_object = self
4554            .scene_graph
4555            .objects
4556            .get(line1_id.0)
4557            .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
4558        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
4559            let kind = line1_object.kind.human_friendly_kind_with_article();
4560            return Err(KclError::refactor(format!(
4561                "This constraint only works on Segments, but you selected {kind}"
4562            )));
4563        };
4564        let Segment::Line(_) = line1_segment else {
4565            return Err(KclError::refactor(format!(
4566                "Only lines can be made {}, but you selected {}",
4567                angle_kind.to_function_name(),
4568                line1_segment.human_friendly_kind_with_article(),
4569            )));
4570        };
4571        let line1_ast = self.line_id_to_ast_reference(line1_id, new_ast)?;
4572
4573        // Create the parallel() or perpendicular() call.
4574        let call_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4575            callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
4576            unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
4577                ast::ArrayExpression {
4578                    elements: vec![line0_ast, line1_ast],
4579                    digest: None,
4580                    non_code_meta: Default::default(),
4581                },
4582            )))),
4583            arguments: Default::default(),
4584            digest: None,
4585            non_code_meta: Default::default(),
4586        })));
4587
4588        // Add the constraint to the AST of the sketch block.
4589        let (sketch_block_ref, _) = self.mutate_ast(
4590            new_ast,
4591            sketch_id,
4592            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4593        )?;
4594        Ok(sketch_block_ref)
4595    }
4596
4597    async fn add_vertical(
4598        &mut self,
4599        sketch: ObjectId,
4600        vertical: Vertical,
4601        new_ast: &mut ast::Node<ast::Program>,
4602    ) -> Result<AstNodeRef, KclError> {
4603        let sketch_id = sketch;
4604
4605        let first_arg_ast = match vertical {
4606            Vertical::Line { line } => {
4607                // Map the runtime objects back to variable names.
4608                let line_object = self
4609                    .scene_graph
4610                    .objects
4611                    .get(line.0)
4612                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4613                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4614                    let kind = line_object.kind.human_friendly_kind_with_article();
4615                    return Err(KclError::refactor(format!(
4616                        "This constraint only works on Segments, but you selected {kind}"
4617                    )));
4618                };
4619                let Segment::Line(_) = line_segment else {
4620                    return Err(KclError::refactor(format!(
4621                        "Only lines can be made vertical, but you selected {}",
4622                        line_segment.human_friendly_kind_with_article()
4623                    )));
4624                };
4625                self.line_id_to_ast_reference(line, new_ast)?
4626            }
4627            Vertical::Points { points } => {
4628                let point_asts = points
4629                    .iter()
4630                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4631                    .collect::<Result<Vec<_>, _>>()?;
4632                ast::ArrayExpression::new(point_asts).into()
4633            }
4634        };
4635        // Create the vertical() call using shared helper.
4636        let vertical_ast = create_vertical_ast(first_arg_ast);
4637
4638        // Add the line to the AST of the sketch block.
4639        let (sketch_block_ref, _) = self.mutate_ast(
4640            new_ast,
4641            sketch_id,
4642            AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4643        )?;
4644        Ok(sketch_block_ref)
4645    }
4646
4647    async fn execute_after_add_constraint(
4648        &mut self,
4649        ctx: &ExecutorContext,
4650        sketch_id: ObjectId,
4651        sketch_block_ref: AstNodeRef,
4652        new_ast: &mut ast::Node<ast::Program>,
4653    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4654        // Convert to string source to create real source ranges.
4655        let new_source = source_from_ast(new_ast);
4656        // Parse the new KCL source.
4657        let new_program = parse_frontend_mutation_source(
4658            &new_source,
4659            "Error parsing KCL source after adding constraint",
4660            "No AST produced after adding constraint",
4661        )?;
4662        let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4663            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4664                "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4665            )))
4666        })?;
4667
4668        // Truncate after the sketch block for mock execution.
4669        // Use a clone so we don't mutate new_program yet
4670        let mut truncated_program = new_program.clone();
4671        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4672            .map_err(KclErrorWithOutputs::no_outputs)?;
4673
4674        // Execute - if this fails, we haven't modified self yet, so state is safe
4675        let outcome = ctx
4676            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4677            .await?;
4678
4679        let new_object_ids = {
4680            // Extract the constraint ID from the execution outcome using source_range_to_object
4681            let constraint_id = outcome
4682                .source_range_to_object
4683                .get(&constraint_node_ref.range)
4684                .copied()
4685                .ok_or_else(|| {
4686                    KclErrorWithOutputs::from_error_outcome(
4687                        KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4688                        outcome.clone(),
4689                    )
4690                })?;
4691            vec![constraint_id]
4692        };
4693
4694        // Only now, after all operations succeeded, update self.program.
4695        // This ensures state is only modified if everything succeeds.
4696        self.program = new_program;
4697
4698        // Uses MockConfig::default() which has freedom_analysis: true
4699        let outcome = self.update_state_after_exec(outcome, true);
4700
4701        let src_delta = self.commit_var_solutions_to_program(&outcome, "adding constraint")?;
4702        let scene_graph_delta = SceneGraphDelta {
4703            new_graph: self.scene_graph_for_ui(),
4704            invalidates_ids: false,
4705            new_objects: new_object_ids,
4706            exec_outcome: outcome,
4707        };
4708        Ok((src_delta, scene_graph_delta))
4709    }
4710
4711    fn commit_var_solutions_to_program(&mut self, outcome: &ExecOutcome, operation: &str) -> ExecResult<SourceDelta> {
4712        let commit_failure = || {
4713            KclErrorWithOutputs::from_error_outcome(
4714                KclError::refactor(format!("Could not update KCL after {operation}.")),
4715                outcome.clone(),
4716            )
4717        };
4718
4719        let default_length_unit = self.default_length_unit();
4720        let mut settled_ast = self.program.ast.clone();
4721        let mut committed_solver_value = false;
4722        for (var_range, node_path, value) in &outcome.var_solutions {
4723            let Some(lookup) = numeric_literal_at_node_path(&settled_ast, node_path.as_ref(), *var_range) else {
4724                return Err(commit_failure());
4725            };
4726            let new_value = match &lookup {
4727                Some(current_literal) => {
4728                    if !var_solution_needs_commit(current_literal, *value, default_length_unit) {
4729                        continue;
4730                    }
4731                    preserve_var_solution_literal_style(current_literal, *value, default_length_unit)
4732                }
4733                None => {
4734                    // Bare `var` with no initial literal to compare against;
4735                    // always commit, using the module's default length unit as
4736                    // an explicit suffix so the written value carries units.
4737                    Number {
4738                        value: number_value_in_default_length_units(*value, default_length_unit),
4739                        units: default_length_unit.into(),
4740                    }
4741                }
4742            };
4743            committed_solver_value = true;
4744            let source_ref = SourceRef::Simple {
4745                range: *var_range,
4746                node_path: node_path.clone(),
4747            };
4748            mutate_ast_node_by_source_ref(
4749                &mut settled_ast,
4750                &source_ref,
4751                AstMutateCommand::EditVarInitialValue { value: new_value },
4752            )
4753            .map_err(|_| commit_failure())?;
4754        }
4755
4756        if !committed_solver_value {
4757            return Ok(SourceDelta {
4758                text: self.program.original_file_contents.clone(),
4759            });
4760        }
4761
4762        let settled_source = source_from_ast(&settled_ast);
4763        let (settled_program, errors) = Program::parse(&settled_source).map_err(|_| commit_failure())?;
4764        if !errors.is_empty() {
4765            return Err(commit_failure());
4766        }
4767        let Some(settled_program) = settled_program else {
4768            return Err(commit_failure());
4769        };
4770
4771        self.program = settled_program;
4772
4773        Ok(SourceDelta { text: settled_source })
4774    }
4775
4776    // Find constraints that reference the given segments.
4777    fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4778        if segment_ids_set.contains(&segment_id) {
4779            return true;
4780        }
4781
4782        let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4783            return false;
4784        };
4785        let ObjectKind::Segment { segment } = &segment_object.kind else {
4786            return false;
4787        };
4788        let Segment::Point(point) = segment else {
4789            return false;
4790        };
4791
4792        point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4793    }
4794
4795    fn remaining_constraint_segments(
4796        &self,
4797        segments: &[ConstraintSegment],
4798        segment_ids_set: &AhashIndexSet<ObjectId>,
4799    ) -> Vec<ConstraintSegment> {
4800        segments
4801            .iter()
4802            .copied()
4803            .filter(|segment| match segment {
4804                ConstraintSegment::Origin(_) => true,
4805                ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4806            })
4807            .collect()
4808    }
4809
4810    fn find_referenced_constraints(
4811        &self,
4812        sketch_id: ObjectId,
4813        segment_ids_set: &AhashIndexSet<ObjectId>,
4814    ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4815        // Look up the sketch.
4816        let sketch_object = self
4817            .scene_graph
4818            .objects
4819            .get(sketch_id.0)
4820            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4821        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4822            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4823        };
4824        let segment_or_owner_matches = |segment_id: ObjectId| {
4825            if segment_ids_set.contains(&segment_id) {
4826                return true;
4827            }
4828            let segment_object = self.scene_graph.objects.get(segment_id.0);
4829            if let Some(obj) = segment_object
4830                && let ObjectKind::Segment { segment } = &obj.kind
4831            {
4832                match segment {
4833                    Segment::Point(point) => point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4834                    Segment::Line(line) => line.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4835                    _ => false,
4836                }
4837            } else {
4838                false
4839            }
4840        };
4841        let mut constraint_ids_set = AhashIndexSet::default();
4842        for constraint_id in &sketch.constraints {
4843            let constraint_object = self
4844                .scene_graph
4845                .objects
4846                .get(constraint_id.0)
4847                .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4848            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4849                return Err(KclError::refactor(format!(
4850                    "Object is not a constraint, it is {}",
4851                    constraint_object.kind.human_friendly_kind_with_article()
4852                )));
4853            };
4854            let depends_on_segment = match constraint {
4855                Constraint::Coincident(c) => c.segment_ids().any(segment_or_owner_matches),
4856                Constraint::Distance(d) => d.segment_ids().any(segment_or_owner_matches),
4857                Constraint::Fixed(fixed) => fixed
4858                    .points
4859                    .iter()
4860                    .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4861                Constraint::Radius(r) => segment_or_owner_matches(r.arc),
4862                Constraint::Diameter(d) => segment_or_owner_matches(d.arc),
4863                Constraint::EqualRadius(equal_radius) => {
4864                    equal_radius.input.iter().copied().any(segment_or_owner_matches)
4865                }
4866                Constraint::HorizontalDistance(d) => d.segment_ids().any(segment_or_owner_matches),
4867                Constraint::VerticalDistance(d) => d.segment_ids().any(segment_or_owner_matches),
4868                Constraint::Horizontal(h) => match h {
4869                    Horizontal::Line { line } => segment_or_owner_matches(*line),
4870                    Horizontal::Points { points } => points.iter().any(|point| match point {
4871                        ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4872                        ConstraintSegment::Origin(_) => false,
4873                    }),
4874                },
4875                Constraint::Vertical(v) => match v {
4876                    Vertical::Line { line } => segment_or_owner_matches(*line),
4877                    Vertical::Points { points } => points.iter().any(|point| match point {
4878                        ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4879                        ConstraintSegment::Origin(_) => false,
4880                    }),
4881                },
4882                Constraint::LinesEqualLength(lines_equal_length) => {
4883                    lines_equal_length.lines.iter().copied().any(segment_or_owner_matches)
4884                }
4885                Constraint::Midpoint(midpoint) => {
4886                    segment_or_owner_matches(midpoint.segment)
4887                        || matches!(
4888                            midpoint.point,
4889                            ConstraintSegment::Segment(point) if segment_or_owner_matches(point)
4890                        )
4891                }
4892                Constraint::Parallel(parallel) => parallel.lines.iter().copied().any(segment_or_owner_matches),
4893                Constraint::Perpendicular(perpendicular) => {
4894                    perpendicular.lines.iter().copied().any(segment_or_owner_matches)
4895                }
4896                Constraint::Angle(angle) => angle.lines.iter().copied().any(segment_or_owner_matches),
4897                Constraint::Symmetric(symmetric) => {
4898                    segment_or_owner_matches(symmetric.axis)
4899                        || symmetric.input.iter().copied().any(segment_or_owner_matches)
4900                }
4901                Constraint::Tangent(tangent) => tangent.input.iter().copied().any(segment_or_owner_matches),
4902            };
4903            if depends_on_segment {
4904                constraint_ids_set.insert(*constraint_id);
4905            }
4906        }
4907        Ok(constraint_ids_set)
4908    }
4909
4910    fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4911        let mut outcome = outcome;
4912        self.solid_references = solid_references_from_variables(&self.program.ast, &outcome.variables);
4913        let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4914
4915        if freedom_analysis_ran {
4916            // When freedom analysis ran, replace the cache entirely with new values
4917            // Don't merge with old values since IDs might have changed
4918            self.point_freedom_cache.clear();
4919            for new_obj in &new_objects {
4920                if let ObjectKind::Segment {
4921                    segment: crate::front::Segment::Point(point),
4922                } = &new_obj.kind
4923                {
4924                    self.point_freedom_cache.insert(new_obj.id, point.freedom);
4925                }
4926            }
4927            add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4928            // Objects are already correct from the analysis, just use them as-is
4929            self.scene_graph.objects = new_objects;
4930        } else {
4931            // When freedom analysis didn't run, preserve old values and merge
4932            // Before replacing objects, extract and store freedom values from old objects
4933            for old_obj in &self.scene_graph.objects {
4934                if let ObjectKind::Segment {
4935                    segment: crate::front::Segment::Point(point),
4936                } = &old_obj.kind
4937                {
4938                    self.point_freedom_cache.insert(old_obj.id, point.freedom);
4939                }
4940            }
4941
4942            // Update objects, preserving stored freedom values when new is Free (might be default)
4943            let mut updated_objects = Vec::with_capacity(new_objects.len());
4944            for new_obj in new_objects {
4945                let mut obj = new_obj;
4946                if let ObjectKind::Segment {
4947                    segment: crate::front::Segment::Point(point),
4948                } = &mut obj.kind
4949                {
4950                    let new_freedom = point.freedom;
4951                    // When freedom_analysis=false, new values are defaults (Free).
4952                    // Only preserve cached values when new is Free (indicating it's a default, not from analysis).
4953                    // If new is NOT Free, use the new value (it came from somewhere else, maybe conflict detection).
4954                    // Never preserve Conflict from cache - conflicts are transient and should only be set
4955                    // when there are actually unsatisfied constraints.
4956                    match new_freedom {
4957                        Freedom::Free => {
4958                            match self.point_freedom_cache.get(&obj.id).copied() {
4959                                Some(Freedom::Conflict) => {
4960                                    // Don't preserve Conflict - conflicts are transient
4961                                    // Keep it as Free
4962                                }
4963                                Some(Freedom::Fixed) => {
4964                                    // Preserve Fixed cached value
4965                                    point.freedom = Freedom::Fixed;
4966                                }
4967                                Some(Freedom::Free) => {
4968                                    // If stored is also Free, keep Free (no change needed)
4969                                }
4970                                None => {
4971                                    // If no cached value, keep Free (default)
4972                                }
4973                            }
4974                        }
4975                        Freedom::Fixed => {
4976                            // Use new value (already set)
4977                        }
4978                        Freedom::Conflict => {
4979                            // Use new value (already set)
4980                        }
4981                    }
4982                    // Store the new freedom value (even if it's Free, so we know it was set)
4983                    self.point_freedom_cache.insert(obj.id, point.freedom);
4984                }
4985                updated_objects.push(obj);
4986            }
4987
4988            add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4989            self.scene_graph.objects = updated_objects;
4990        }
4991        outcome
4992    }
4993
4994    fn mutate_ast(
4995        &mut self,
4996        ast: &mut ast::Node<ast::Program>,
4997        object_id: ObjectId,
4998        command: AstMutateCommand,
4999    ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5000        let sketch_object = self
5001            .scene_graph
5002            .objects
5003            .get(object_id.0)
5004            .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
5005        mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
5006    }
5007
5008    fn mutate_constraint_label_position(
5009        &mut self,
5010        ast: &mut ast::Node<ast::Program>,
5011        constraint_id: ObjectId,
5012        label_position: Point2d<Number>,
5013    ) -> Result<(), KclError> {
5014        let object = self
5015            .scene_graph
5016            .objects
5017            .get(constraint_id.0)
5018            .ok_or_else(|| KclError::refactor(format!("Object not found: {constraint_id:?}")))?;
5019        if !matches!(
5020            &object.kind,
5021            ObjectKind::Constraint {
5022                constraint: Constraint::Distance(_)
5023                    | Constraint::HorizontalDistance(_)
5024                    | Constraint::VerticalDistance(_)
5025                    | Constraint::Radius(_)
5026                    | Constraint::Diameter(_)
5027                    | Constraint::Angle(_),
5028            }
5029        ) {
5030            return Err(KclError::refactor(format!(
5031                "Object does not support labelPosition: {constraint_id:?}"
5032            )));
5033        }
5034
5035        let label_position = to_ast_point2d_number(&label_position)
5036            .map_err(|err| KclError::refactor(format!("Could not convert label position to AST: {err}")))?;
5037        self.mutate_ast(
5038            ast,
5039            constraint_id,
5040            AstMutateCommand::EditDistanceConstraintLabelPosition { label_position },
5041        )?;
5042        Ok(())
5043    }
5044}
5045
5046fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
5047    // Look up existing sketch.
5048    let sketch_object = scene_graph
5049        .objects
5050        .get(sketch_id.0)
5051        .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
5052    let ObjectKind::Sketch(_) = &sketch_object.kind else {
5053        return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
5054    };
5055    expect_single_node_ref(sketch_object)
5056}
5057
5058fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
5059    match &object.source {
5060        SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
5061            range: *range,
5062            node_path: node_path.clone(),
5063        }),
5064        SourceRef::BackTrace { ranges } => {
5065            let [range] = ranges.as_slice() else {
5066                return Err(KclError::refactor(format!(
5067                    "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
5068                    ranges.len()
5069                )));
5070            };
5071            Ok(AstNodeRef {
5072                range: range.0,
5073                node_path: range.1.clone(),
5074            })
5075        }
5076    }
5077}
5078
5079/// This is a deprecated fall-back implementation. Prefer
5080/// [`only_sketch_block()`] to avoid reliance on source ranges.
5081fn only_sketch_block_from_range(
5082    ast: &mut ast::Node<ast::Program>,
5083    sketch_block_range: SourceRange,
5084    edit_kind: ChangeKind,
5085) -> Result<(), KclError> {
5086    let r1 = sketch_block_range;
5087    let matches_range = |r2: SourceRange| -> bool {
5088        // We may have added items to the sketch block, so the end may not be an
5089        // exact match.
5090        match edit_kind {
5091            ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
5092            // For edit, we don't know whether it grew or shrank.
5093            ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
5094            ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
5095            // No edit should be an exact match.
5096            ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
5097        }
5098    };
5099    let mut found = false;
5100    for item in ast.body.iter_mut() {
5101        match item {
5102            ast::BodyItem::ImportStatement(_) => {}
5103            ast::BodyItem::ExpressionStatement(node) => {
5104                if matches_range(SourceRange::from(&*node))
5105                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5106                {
5107                    sketch_block.is_being_edited = true;
5108                    found = true;
5109                    break;
5110                }
5111            }
5112            ast::BodyItem::VariableDeclaration(node) => {
5113                if matches_range(SourceRange::from(&node.declaration.init))
5114                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5115                {
5116                    sketch_block.is_being_edited = true;
5117                    found = true;
5118                    break;
5119                }
5120            }
5121            ast::BodyItem::TypeDeclaration(_) => {}
5122            ast::BodyItem::ReturnStatement(node) => {
5123                if matches_range(SourceRange::from(&node.argument))
5124                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5125                {
5126                    sketch_block.is_being_edited = true;
5127                    found = true;
5128                    break;
5129                }
5130            }
5131        }
5132    }
5133    if !found {
5134        return Err(KclError::refactor(format!(
5135            "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
5136        )));
5137    }
5138
5139    Ok(())
5140}
5141
5142fn only_sketch_block(
5143    ast: &mut ast::Node<ast::Program>,
5144    sketch_block_ref: &AstNodeRef,
5145    edit_kind: ChangeKind,
5146) -> Result<(), KclError> {
5147    let Some(target_node_path) = &sketch_block_ref.node_path else {
5148        #[cfg(target_arch = "wasm32")]
5149        web_sys::console::warn_1(
5150            &format!(
5151                "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
5152                sketch_block_ref
5153            )
5154            .into(),
5155        );
5156        return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
5157    };
5158    struct MarkSketchBlockBeingEdited<'a> {
5159        target_node_path: &'a ast::NodePath,
5160    }
5161
5162    impl Visitor for MarkSketchBlockBeingEdited<'_> {
5163        type Break = ();
5164        type Continue = ();
5165
5166        fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5167            if let NodeMut::SketchBlock(sketch_block) = node
5168                && sketch_block.node_path.as_ref() == Some(self.target_node_path)
5169            {
5170                sketch_block.is_being_edited = true;
5171                return TraversalReturn::new_break(());
5172            }
5173            TraversalReturn::new_continue(())
5174        }
5175
5176        fn finish(&mut self, _node: NodeMut<'_>) {}
5177    }
5178
5179    let mut marker = MarkSketchBlockBeingEdited { target_node_path };
5180    let found = dfs_mut(ast, &mut marker).is_break();
5181    if !found {
5182        return Err(KclError::refactor(format!(
5183            "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
5184        )));
5185    }
5186
5187    Ok(())
5188}
5189
5190fn sketch_on_ast_expr(
5191    ast: &mut ast::Node<ast::Program>,
5192    scene_graph: &SceneGraph,
5193    solid_references: &HashMap<Uuid, SolidAstReference>,
5194    on: &Plane,
5195) -> Result<ast::Expr, KclError> {
5196    match on {
5197        Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
5198        Plane::Object(object_id) => {
5199            let on_object = scene_graph
5200                .objects
5201                .get(object_id.0)
5202                .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
5203            if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
5204                return Ok(face_expr);
5205            }
5206            get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
5207        }
5208        Plane::PrimitiveFace(face) => {
5209            let solid_expr = solid_expr_for_engine_id(solid_references, face.solid_id).ok_or_else(|| {
5210                KclError::refactor(format!(
5211                    "Could not resolve a KCL solid for selected primitive face: solid_id={}",
5212                    face.solid_id
5213                ))
5214            })?;
5215            let face_id_expr = create_face_id_ast(solid_expr.clone(), face.index);
5216            Ok(create_face_of_ast(solid_expr, face_id_expr))
5217        }
5218    }
5219}
5220
5221fn solid_references_from_variables(
5222    ast: &ast::Node<ast::Program>,
5223    variables: &IndexMap<String, KclValueView>,
5224) -> HashMap<Uuid, SolidAstReference> {
5225    let mut references = HashMap::new();
5226
5227    // In-place operations such as shell reuse their input solid's engine ID.
5228    // Later variables overwrite earlier references so mutations target the result.
5229    for item in &ast.body {
5230        let ast::BodyItem::VariableDeclaration(declaration) = item else {
5231            continue;
5232        };
5233        let name = &declaration.declaration.id.name;
5234        let Some(value) = variables.get(name) else {
5235            continue;
5236        };
5237
5238        match value {
5239            KclValueView::Solid { value } => {
5240                references.insert(
5241                    value.id,
5242                    SolidAstReference {
5243                        variable_name: name.clone(),
5244                        output_index: None,
5245                    },
5246                );
5247            }
5248            KclValueView::Tuple { value } | KclValueView::HomArray { value } => {
5249                for (output_index, entry) in value.iter().enumerate() {
5250                    if let KclValueView::Solid { value } = entry {
5251                        references.insert(
5252                            value.id,
5253                            SolidAstReference {
5254                                variable_name: name.clone(),
5255                                output_index: Some(output_index),
5256                            },
5257                        );
5258                    }
5259                }
5260            }
5261            _ => {}
5262        }
5263    }
5264
5265    references
5266}
5267
5268fn solid_expr_for_engine_id(solid_references: &HashMap<Uuid, SolidAstReference>, solid_id: Uuid) -> Option<ast::Expr> {
5269    let reference = solid_references.get(&solid_id)?;
5270    let solid_expr = ast_name_expr(reference.variable_name.clone());
5271    Some(indexed_solid_expr_for_sweep_output(solid_expr, reference.output_index))
5272}
5273
5274fn sketch_face_of_scene_object_ast_expr(
5275    ast: &mut ast::Node<ast::Program>,
5276    on_object: &crate::front::Object,
5277) -> Result<Option<ast::Expr>, KclError> {
5278    match &on_object.kind {
5279        ObjectKind::Wall(wall) => {
5280            let solid_ref = get_or_insert_ast_reference(
5281                ast,
5282                &source_ref_from_source_ref_range(&wall.source.solid),
5283                "solid",
5284                None,
5285            )?;
5286            let ast::Expr::Name(solid_name_expr) = solid_ref else {
5287                return Err(KclError::refactor(format!(
5288                    "Could not resolve solid reference for selected wall: artifact_id={:?}",
5289                    on_object.artifact_id
5290                )));
5291            };
5292            let solid_expr = indexed_solid_expr_for_sweep_output(
5293                ast_name_expr(solid_name_expr.name.name.clone()),
5294                wall.solid_output_index,
5295            );
5296            let sweep_ref = get_or_insert_ast_reference(
5297                ast,
5298                &source_ref_from_source_ref_range(&wall.source.sweep),
5299                "solid",
5300                None,
5301            )?;
5302            let ast::Expr::Name(sweep_name_expr) = sweep_ref else {
5303                return Err(KclError::refactor(format!(
5304                    "Could not resolve sweep reference for selected wall: artifact_id={:?}",
5305                    on_object.artifact_id
5306                )));
5307            };
5308            let sweep_name = sweep_name_expr.name.name.clone();
5309            let segment_ref = get_or_insert_ast_reference(
5310                ast,
5311                &source_ref_from_source_ref_range(&wall.source.segment),
5312                LINE_VARIABLE,
5313                None,
5314            )?;
5315
5316            let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &sweep_name).or_else(|| {
5317                wall.source
5318                    .path
5319                    .as_ref()
5320                    .and_then(|path_source| region_name_from_path_source(ast, path_source))
5321            }) {
5322                let ast::Expr::Name(segment_name_expr) = segment_ref else {
5323                    return Err(KclError::refactor(format!(
5324                        "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
5325                        on_object.artifact_id
5326                    )));
5327                };
5328                create_member_expression(
5329                    create_member_expression(ast_name_expr(region_name), "tags"),
5330                    &segment_name_expr.name.name,
5331                )
5332            } else {
5333                segment_ref
5334            };
5335
5336            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5337        }
5338        ObjectKind::Cap(cap) => {
5339            let solid_ref =
5340                get_or_insert_ast_reference(ast, &source_ref_from_source_ref_range(&cap.source.solid), "solid", None)?;
5341            let ast::Expr::Name(solid_name_expr) = solid_ref else {
5342                return Err(KclError::refactor(format!(
5343                    "Could not resolve solid reference for selected cap: artifact_id={:?}",
5344                    on_object.artifact_id
5345                )));
5346            };
5347            let solid_expr = indexed_solid_expr_for_sweep_output(
5348                ast_name_expr(solid_name_expr.name.name.clone()),
5349                cap.solid_output_index,
5350            );
5351            // TODO: change this to explicit tag references with tagStart/tagEnd mutations
5352            let face_expr = match cap.kind {
5353                crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
5354                crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
5355            };
5356
5357            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5358        }
5359        _ => Ok(None),
5360    }
5361}
5362
5363fn indexed_solid_expr_for_sweep_output(solid_expr: ast::Expr, solid_output_index: Option<usize>) -> ast::Expr {
5364    match solid_output_index {
5365        Some(output_index) => create_index_expression(solid_expr, output_index),
5366        None => solid_expr,
5367    }
5368}
5369
5370fn source_ref_from_source_ref_range(source: &SourceRefRange) -> SourceRef {
5371    SourceRef::Simple {
5372        range: source.range,
5373        node_path: source.node_path.clone(),
5374    }
5375}
5376
5377fn region_name_from_path_source(ast: &ast::Node<ast::Program>, path_source: &SourceRefRange) -> Option<String> {
5378    let source_ref = source_ref_from_source_ref_range(path_source);
5379    let candidate = variable_name_containing_source_ref(ast, &source_ref)?;
5380    let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5381        return None;
5382    };
5383    let ast::Expr::CallExpressionKw(region_call) = &region_decl.init else {
5384        return None;
5385    };
5386    if region_call.callee.name.name != "region" {
5387        return None;
5388    }
5389    Some(candidate)
5390}
5391
5392fn downstream_composite_code_ref_for_source(artifact_graph: &ArtifactGraph, source_id: ArtifactId) -> Option<&CodeRef> {
5393    let mut current_id = source_id;
5394    let mut current_composite = None;
5395    let mut visited = HashSet::new();
5396
5397    while visited.insert(current_id) {
5398        let next_composite_id = downstream_composite_id_for_solid_source(artifact_graph, current_id);
5399
5400        let Some(composite_id) = next_composite_id else {
5401            break;
5402        };
5403        let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id) else {
5404            break;
5405        };
5406
5407        current_id = composite.id;
5408        current_composite = Some(composite);
5409
5410        if !composite.consumed {
5411            break;
5412        }
5413    }
5414
5415    current_composite.map(|composite| &composite.code_ref)
5416}
5417
5418fn downstream_composite_id_for_solid_source(
5419    artifact_graph: &ArtifactGraph,
5420    source_id: ArtifactId,
5421) -> Option<ArtifactId> {
5422    // Source is a path, find its solid.
5423    if let Some(Artifact::Path(path)) = artifact_graph.get(&source_id)
5424        && let Some(composite_id) = path.composite_solid_id
5425        && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5426        && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5427    {
5428        return Some(composite_id);
5429    }
5430
5431    // Source is a sweep, find its path -> then find the solid
5432    for artifact in artifact_graph.values() {
5433        if let Artifact::Path(path) = artifact
5434            && path.sweep_id == Some(source_id)
5435            && let Some(composite_id) = path.composite_solid_id
5436            && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5437            && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5438        {
5439            return Some(composite_id);
5440        }
5441    }
5442
5443    // Source is a solid, find its downstream solid.
5444    artifact_graph.values().find_map(|artifact| {
5445        let Artifact::CompositeSolid(composite) = artifact else {
5446            return None;
5447        };
5448        composite_contains_input(&composite.solid_ids, &composite.tool_ids, source_id).then_some(composite.id)
5449    })
5450}
5451
5452fn composite_contains_path_input(
5453    solid_ids: &[ArtifactId],
5454    tool_ids: &[ArtifactId],
5455    path_id: ArtifactId,
5456    solid2d_id: Option<ArtifactId>,
5457) -> bool {
5458    composite_contains_input(solid_ids, tool_ids, path_id)
5459        || solid2d_id.is_some_and(|solid2d_id| composite_contains_input(solid_ids, tool_ids, solid2d_id))
5460}
5461
5462fn composite_contains_input(solid_ids: &[ArtifactId], tool_ids: &[ArtifactId], input_id: ArtifactId) -> bool {
5463    solid_ids.contains(&input_id) || tool_ids.contains(&input_id)
5464}
5465
5466fn code_ref_source_ref_range(code_ref: &CodeRef) -> SourceRefRange {
5467    let node_path = (!code_ref.node_path.is_empty()).then(|| code_ref.node_path.clone());
5468    SourceRefRange {
5469        range: code_ref.range,
5470        node_path,
5471    }
5472}
5473
5474fn solid_output_index_for_sweep(
5475    artifact_graph: &ArtifactGraph,
5476    sweep_id: ArtifactId,
5477    sweep_code_ref: &CodeRef,
5478) -> Option<usize> {
5479    // Constituent sweeps are implementation details of one composite body,
5480    // even when cloning gives them the same CodeRef as the composite root.
5481    if downstream_composite_id_for_solid_source(artifact_graph, sweep_id).is_some() {
5482        return None;
5483    }
5484
5485    let sibling_sweeps = artifact_graph
5486        .values()
5487        .filter_map(|artifact| match artifact {
5488            Artifact::Sweep(sweep)
5489                if sweep.code_ref.range == sweep_code_ref.range
5490                    && sweep.code_ref.node_path == sweep_code_ref.node_path =>
5491            {
5492                Some(sweep)
5493            }
5494            _ => None,
5495        })
5496        .collect::<Vec<_>>();
5497
5498    if sibling_sweeps.len() <= 1 {
5499        return None;
5500    }
5501
5502    sibling_sweeps
5503        .iter()
5504        .position(|sibling_sweep| sibling_sweep.id == sweep_id)
5505}
5506
5507fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
5508    let mut existing_artifact_ids = scene_objects
5509        .iter()
5510        .map(|object| object.artifact_id)
5511        .collect::<HashSet<_>>();
5512
5513    for artifact in artifact_graph.values() {
5514        match artifact {
5515            Artifact::Wall(wall) => {
5516                if existing_artifact_ids.contains(&wall.id) {
5517                    continue;
5518                }
5519
5520                let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
5521                    Artifact::Segment(segment) => Some(segment),
5522                    _ => None,
5523                }) else {
5524                    continue;
5525                };
5526                let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
5527                    Artifact::Sweep(sweep) => Some(sweep),
5528                    _ => None,
5529                }) else {
5530                    continue;
5531                };
5532                let source_segment = segment
5533                    .original_seg_id
5534                    .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
5535                    .and_then(|artifact| match artifact {
5536                        Artifact::Segment(segment) => Some(segment),
5537                        _ => None,
5538                    })
5539                    .unwrap_or(segment);
5540                let solid_code_ref =
5541                    downstream_composite_code_ref_for_source(artifact_graph, wall.sweep_id).unwrap_or(&sweep.code_ref);
5542                let path_code_ref = artifact_graph
5543                    .get(&segment.path_id)
5544                    .or_else(|| artifact_graph.get(&sweep.path_id))
5545                    .and_then(|artifact| match artifact {
5546                        Artifact::Path(path) => Some(&path.code_ref),
5547                        _ => None,
5548                    });
5549                let source = WallSource {
5550                    solid: code_ref_source_ref_range(solid_code_ref),
5551                    sweep: code_ref_source_ref_range(&sweep.code_ref),
5552                    path: path_code_ref.map(code_ref_source_ref_range),
5553                    segment: code_ref_source_ref_range(&source_segment.code_ref),
5554                };
5555                let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5556                    && solid_code_ref.node_path == sweep.code_ref.node_path)
5557                    .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5558                    .flatten();
5559                let object_source = source_ref_from_source_ref_range(&source.solid);
5560                let id = ObjectId(scene_objects.len());
5561                scene_objects.push(crate::front::Object {
5562                    id,
5563                    kind: ObjectKind::Wall(crate::frontend::api::Wall {
5564                        id,
5565                        source,
5566                        solid_output_index,
5567                    }),
5568                    label: Default::default(),
5569                    comments: Default::default(),
5570                    artifact_id: wall.id,
5571                    source: object_source,
5572                });
5573                existing_artifact_ids.insert(wall.id);
5574            }
5575            Artifact::Cap(cap) => {
5576                if existing_artifact_ids.contains(&cap.id) {
5577                    continue;
5578                }
5579
5580                let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
5581                    Artifact::Sweep(sweep) => Some(sweep),
5582                    _ => None,
5583                }) else {
5584                    continue;
5585                };
5586                let id = ObjectId(scene_objects.len());
5587                let kind = match cap.sub_type {
5588                    CapSubType::Start => crate::frontend::api::CapKind::Start,
5589                    CapSubType::End => crate::frontend::api::CapKind::End,
5590                };
5591                let solid_code_ref =
5592                    downstream_composite_code_ref_for_source(artifact_graph, cap.sweep_id).unwrap_or(&sweep.code_ref);
5593                let source = CapSource {
5594                    solid: code_ref_source_ref_range(solid_code_ref),
5595                    sweep: code_ref_source_ref_range(&sweep.code_ref),
5596                };
5597                let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5598                    && solid_code_ref.node_path == sweep.code_ref.node_path)
5599                    .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5600                    .flatten();
5601                let object_source = source_ref_from_source_ref_range(&source.solid);
5602                scene_objects.push(crate::front::Object {
5603                    id,
5604                    kind: ObjectKind::Cap(crate::frontend::api::Cap {
5605                        id,
5606                        kind,
5607                        source,
5608                        solid_output_index,
5609                    }),
5610                    label: Default::default(),
5611                    comments: Default::default(),
5612                    artifact_id: cap.id,
5613                    source: object_source,
5614                });
5615                existing_artifact_ids.insert(cap.id);
5616            }
5617            _ => {}
5618        }
5619    }
5620}
5621
5622fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
5623    use crate::engine::PlaneName;
5624
5625    match name {
5626        PlaneName::Xy => ast_name_expr("XY".to_owned()),
5627        PlaneName::Xz => ast_name_expr("XZ".to_owned()),
5628        PlaneName::Yz => ast_name_expr("YZ".to_owned()),
5629        PlaneName::NegXy => negated_plane_ast_expr("XY"),
5630        PlaneName::NegXz => negated_plane_ast_expr("XZ"),
5631        PlaneName::NegYz => negated_plane_ast_expr("YZ"),
5632    }
5633}
5634
5635fn negated_plane_ast_expr(name: &str) -> ast::Expr {
5636    ast::Expr::UnaryExpression(BoxNode::new(ast::UnaryExpression::new(
5637        ast::UnaryOperator::Neg,
5638        ast::BinaryPart::Name(BoxNode::new(ast_name(name.to_owned()))),
5639    )))
5640}
5641
5642fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
5643    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
5644        callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
5645        unlabeled: Some(solid_expr),
5646        arguments: vec![ast::LabeledArg {
5647            label: Some(ast::Identifier::new("face")),
5648            arg: face_expr,
5649        }],
5650        digest: None,
5651        non_code_meta: Default::default(),
5652    })))
5653}
5654
5655fn create_face_id_ast(solid_expr: ast::Expr, index: usize) -> ast::Expr {
5656    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
5657        callee: ast::Node::no_src(ast_sketch2_name("faceId")),
5658        unlabeled: Some(solid_expr),
5659        arguments: vec![ast::LabeledArg {
5660            label: Some(ast::Identifier::new("index")),
5661            arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(
5662                ast::NumericLiteral {
5663                    value: index as f64,
5664                    suffix: NumericSuffix::None,
5665                    raw: index.to_string(),
5666                    digest: None,
5667                },
5668            )))),
5669        }],
5670        digest: None,
5671        non_code_meta: Default::default(),
5672    })))
5673}
5674
5675fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
5676    let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
5677        return None;
5678    };
5679    let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
5680        return None;
5681    };
5682    if !matches!(
5683        sweep_call.callee.name.name.as_str(),
5684        "extrude" | "revolve" | "sweep" | "loft"
5685    ) {
5686        return None;
5687    }
5688    let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
5689        return None;
5690    };
5691    let candidate = region_name_expr.name.name.clone();
5692    let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5693        return None;
5694    };
5695    let ast::Expr::CallExpressionKw(region_call) = &region_decl.init else {
5696        return None;
5697    };
5698    if region_call.callee.name.name != "region" {
5699        return None;
5700    }
5701    Some(candidate)
5702}
5703
5704/// Return the AST expression referencing the variable at the given source ref.
5705/// If no such variable exists, insert a new variable declaration with the given
5706/// prefix.
5707///
5708/// This may return a complex expression referencing properties of the variable
5709/// (e.g., `line1.start`).
5710fn get_or_insert_ast_reference(
5711    ast: &mut ast::Node<ast::Program>,
5712    source_ref: &SourceRef,
5713    prefix: &str,
5714    property: Option<&str>,
5715) -> Result<ast::Expr, KclError> {
5716    let command = AstMutateCommand::AddVariableDeclaration {
5717        prefix: prefix.to_owned(),
5718    };
5719    let ret = match mutate_ast_node_by_source_ref(ast, source_ref, command) {
5720        Ok((_, ret)) => ret,
5721        Err(err) => {
5722            if let Some(var_name) = variable_name_containing_source_ref(ast, source_ref) {
5723                AstMutateCommandReturn::Name(var_name)
5724            } else {
5725                return Err(err);
5726            }
5727        }
5728    };
5729    let AstMutateCommandReturn::Name(var_name) = ret else {
5730        return Err(KclError::refactor(
5731            "Expected variable name returned from AddVariableDeclaration".to_owned(),
5732        ));
5733    };
5734    let var_expr = ast::Expr::Name(BoxNode::new(ast::Name::new(&var_name)));
5735    let Some(property) = property else {
5736        // No property; just return the variable name.
5737        return Ok(var_expr);
5738    };
5739
5740    Ok(create_member_expression(var_expr, property))
5741}
5742
5743fn variable_name_containing_source_ref(ast: &ast::Node<ast::Program>, source_ref: &SourceRef) -> Option<String> {
5744    let source_range = match source_ref {
5745        SourceRef::Simple { range, .. } => *range,
5746        SourceRef::BackTrace { ranges } => {
5747            let [range] = ranges.as_slice() else {
5748                return None;
5749            };
5750            range.0
5751        }
5752    };
5753    ast.body.iter().find_map(|item| {
5754        let ast::BodyItem::VariableDeclaration(var_decl) = item else {
5755            return None;
5756        };
5757        let init_range = SourceRange::from(&var_decl.declaration.init);
5758        let source_is_inside_init = init_range.module_id() == source_range.module_id()
5759            && init_range.start() <= source_range.start()
5760            && source_range.end() <= init_range.end();
5761        if matches!(&var_decl.declaration.init, ast::Expr::SketchBlock(_))
5762            && init_range != source_range
5763            && source_is_inside_init
5764        {
5765            return None;
5766        }
5767        source_is_inside_init.then(|| var_decl.name().to_owned())
5768    })
5769}
5770
5771fn mutate_ast_node_by_source_ref(
5772    ast: &mut ast::Node<ast::Program>,
5773    source_ref: &SourceRef,
5774    command: AstMutateCommand,
5775) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5776    let (source_range, node_path) = match source_ref {
5777        SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
5778        SourceRef::BackTrace { ranges } => {
5779            let [range] = ranges.as_slice() else {
5780                return Err(KclError::refactor(format!(
5781                    "Expected single source ref, got {}; ranges={ranges:#?}",
5782                    ranges.len(),
5783                )));
5784            };
5785            (range.0, range.1.clone())
5786        }
5787    };
5788    let mut context = AstMutateContext {
5789        source_range,
5790        node_path,
5791        command,
5792        defined_names_stack: Default::default(),
5793    };
5794    let control = dfs_mut(ast, &mut context);
5795    match control {
5796        ControlFlow::Continue(_) => Err(KclError::refactor(
5797            "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
5798        )),
5799        ControlFlow::Break(break_value) => break_value,
5800    }
5801}
5802
5803#[derive(Debug)]
5804struct AstMutateContext {
5805    source_range: SourceRange,
5806    node_path: Option<ast::NodePath>,
5807    command: AstMutateCommand,
5808    defined_names_stack: Vec<HashSet<String>>,
5809}
5810
5811#[derive(Debug)]
5812#[allow(clippy::large_enum_variant)]
5813enum AstMutateCommand {
5814    /// Add an expression statement to the sketch block.
5815    AddSketchBlockExprStmt {
5816        expr: ast::Expr,
5817    },
5818    /// Add a variable declaration to the sketch block (e.g. `line1 = line(...)`).
5819    AddSketchBlockVarDecl {
5820        prefix: String,
5821        expr: ast::Expr,
5822    },
5823    AddVariableDeclaration {
5824        prefix: String,
5825    },
5826    EditPoint {
5827        at: ast::Expr,
5828    },
5829    EditLine {
5830        start: ast::Expr,
5831        end: ast::Expr,
5832        construction: Option<bool>,
5833    },
5834    EditArc {
5835        start: ast::Expr,
5836        end: ast::Expr,
5837        center: ast::Expr,
5838        direction: Option<ArcDirection>,
5839        construction: Option<bool>,
5840    },
5841    EditCircle {
5842        start: ast::Expr,
5843        center: ast::Expr,
5844        construction: Option<bool>,
5845    },
5846    EditControlPointSpline {
5847        points: ast::Expr,
5848        construction: Option<bool>,
5849    },
5850    EditConstraintValue {
5851        value: ast::BinaryPart,
5852    },
5853    EditAngleConstraint {
5854        call: ast::BinaryPart,
5855        value: ast::BinaryPart,
5856    },
5857    EditDistanceConstraint {
5858        call: ast::BinaryPart,
5859        value: ast::BinaryPart,
5860    },
5861    EditDistanceConstraintLabelPosition {
5862        label_position: ast::Expr,
5863    },
5864    EditCallUnlabeled {
5865        arg: ast::Expr,
5866    },
5867    EditVarInitialValue {
5868        value: Number,
5869    },
5870    DeleteNode,
5871}
5872
5873impl AstMutateCommand {
5874    fn needs_defined_names_stack(&self) -> bool {
5875        matches!(
5876            self,
5877            AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
5878        )
5879    }
5880}
5881
5882#[derive(Debug)]
5883enum AstMutateCommandReturn {
5884    None,
5885    Name(String),
5886}
5887
5888#[derive(Debug, Clone)]
5889struct AstNodeRef {
5890    range: SourceRange,
5891    node_path: Option<ast::NodePath>,
5892}
5893
5894impl<T> From<&ast::Node<T>> for AstNodeRef {
5895    fn from(value: &ast::Node<T>) -> Self {
5896        AstNodeRef {
5897            range: value.into(),
5898            node_path: value.node_path.clone(),
5899        }
5900    }
5901}
5902
5903impl From<&ast::BodyItem> for AstNodeRef {
5904    fn from(value: &ast::BodyItem) -> Self {
5905        match value {
5906            ast::BodyItem::ImportStatement(node) => AstNodeRef {
5907                range: node.into(),
5908                node_path: node.node_path.clone(),
5909            },
5910            ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
5911                range: node.into(),
5912                node_path: node.node_path.clone(),
5913            },
5914            ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
5915                range: node.into(),
5916                node_path: node.node_path.clone(),
5917            },
5918            ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
5919                range: node.into(),
5920                node_path: node.node_path.clone(),
5921            },
5922            ast::BodyItem::ReturnStatement(node) => AstNodeRef {
5923                range: node.into(),
5924                node_path: node.node_path.clone(),
5925            },
5926        }
5927    }
5928}
5929
5930impl From<&ast::Expr> for AstNodeRef {
5931    fn from(value: &ast::Expr) -> Self {
5932        AstNodeRef {
5933            range: SourceRange::from(value),
5934            node_path: value.node_path().cloned(),
5935        }
5936    }
5937}
5938
5939impl From<&AstMutateContext> for AstNodeRef {
5940    fn from(value: &AstMutateContext) -> Self {
5941        AstNodeRef {
5942            range: value.source_range,
5943            node_path: value.node_path.clone(),
5944        }
5945    }
5946}
5947
5948impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5949    type Error = crate::walk::AstNodeError;
5950
5951    fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5952        Ok(AstNodeRef {
5953            range: SourceRange::try_from(value)?,
5954            node_path: value.try_into()?,
5955        })
5956    }
5957}
5958
5959impl From<AstNodeRef> for SourceRange {
5960    fn from(value: AstNodeRef) -> Self {
5961        value.range
5962    }
5963}
5964
5965impl Visitor for AstMutateContext {
5966    type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5967    type Continue = ();
5968
5969    fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5970        filter_and_process(self, node)
5971    }
5972
5973    fn finish(&mut self, node: NodeMut<'_>) {
5974        match &node {
5975            NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
5976                self.defined_names_stack.pop();
5977            }
5978            _ => {}
5979        }
5980    }
5981}
5982
5983fn filter_and_process(
5984    ctx: &mut AstMutateContext,
5985    node: NodeMut,
5986) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
5987    let Ok(node_range) = SourceRange::try_from(&node) else {
5988        // Nodes that can't be converted to a range aren't interesting.
5989        return TraversalReturn::new_continue(());
5990    };
5991    // If we're adding a variable declaration, we need to look at variable
5992    // declaration expressions to see if it already has a variable, before
5993    // continuing. The variable declaration's source range won't match the
5994    // target; its init expression will.
5995    if let NodeMut::VariableDeclaration(var_decl) = &node {
5996        let expr_range = SourceRange::from(&var_decl.declaration.init);
5997        let expr_node_path = var_decl.declaration.init.node_path();
5998        if source_ref_matches(ctx, expr_range, expr_node_path) {
5999            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
6000                // We found the variable declaration expression. It doesn't need
6001                // to be added.
6002                return TraversalReturn::new_break(Ok((
6003                    AstNodeRef::from(&**var_decl),
6004                    AstMutateCommandReturn::Name(var_decl.name().to_owned()),
6005                )));
6006            }
6007            if let AstMutateCommand::DeleteNode = &ctx.command {
6008                // We found the variable declaration. Delete the variable along
6009                // with the segment.
6010                return TraversalReturn {
6011                    mutate_body_item: MutateBodyItem::Delete,
6012                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
6013                };
6014            }
6015        }
6016    }
6017    // Similar thing with expression statement. We need to look at the
6018    // expression inside it.
6019    if let NodeMut::ExpressionStatement(expr_stmt) = &node {
6020        let expr_range = SourceRange::from(&expr_stmt.expression);
6021        let expr_node_path = expr_stmt.expression.node_path();
6022        if source_ref_matches(ctx, expr_range, expr_node_path) {
6023            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
6024                // We found the node wrapped in an expression statement. Process
6025                // the statement.
6026                let Ok(node_ref) = AstNodeRef::try_from(&node) else {
6027                    return TraversalReturn::new_continue(());
6028                };
6029                return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
6030            }
6031            if let AstMutateCommand::DeleteNode = &ctx.command {
6032                // We found the node wrapped in an expression statement. Delete
6033                // the whole statement.
6034                return TraversalReturn {
6035                    mutate_body_item: MutateBodyItem::Delete,
6036                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
6037                };
6038            }
6039        }
6040    }
6041
6042    if ctx.command.needs_defined_names_stack() {
6043        if let NodeMut::Program(program) = &node {
6044            ctx.defined_names_stack.push(find_defined_names(*program));
6045        } else if let NodeMut::SketchBlock(block) = &node {
6046            ctx.defined_names_stack.push(find_defined_names(&block.body));
6047        }
6048    }
6049
6050    // Make sure the node matches the source ref.
6051    let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
6052    if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
6053        return TraversalReturn::new_continue(());
6054    }
6055    let Ok(node_ref) = AstNodeRef::try_from(&node) else {
6056        return TraversalReturn::new_continue(());
6057    };
6058    process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
6059}
6060
6061fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
6062    match &ctx.node_path {
6063        Some(target) => Some(target) == node_path,
6064        None => node_range == ctx.source_range,
6065    }
6066}
6067
6068fn is_angle_constraint_call_name(name: &str) -> bool {
6069    matches!(name, ANGLE_FN | ANGLE_DIMENSION_FN)
6070}
6071
6072fn is_distance_constraint_call_name(name: &str) -> bool {
6073    matches!(name, DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN)
6074}
6075
6076fn is_constraint_call_name(name: &str) -> bool {
6077    matches!(
6078        name,
6079        DISTANCE_FN
6080            | HORIZONTAL_DISTANCE_FN
6081            | VERTICAL_DISTANCE_FN
6082            | RADIUS_FN
6083            | DIAMETER_FN
6084            | ANGLE_FN
6085            | ANGLE_DIMENSION_FN
6086    )
6087}
6088
6089fn constraint_supports_label_position(part: &mut ast::BinaryPart) -> Option<&mut BoxNode<CallExpressionKw>> {
6090    if let ast::BinaryPart::CallExpressionKw(call) = part
6091        && is_constraint_call_name(call.callee.name.name.as_str())
6092    {
6093        Some(call)
6094    } else {
6095        None
6096    }
6097}
6098
6099fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
6100    match &ctx.command {
6101        AstMutateCommand::AddSketchBlockExprStmt { expr } => {
6102            if let NodeMut::SketchBlock(sketch_block) = node {
6103                sketch_block
6104                    .body
6105                    .items
6106                    .push(ast::BodyItem::ExpressionStatement(ast::Node {
6107                        inner: ast::ExpressionStatement {
6108                            expression: expr.clone(),
6109                            digest: None,
6110                        },
6111                        start: Default::default(),
6112                        end: Default::default(),
6113                        module_id: Default::default(),
6114                        node_path: None,
6115                        outer_attrs: Default::default(),
6116                        pre_comments: Default::default(),
6117                        comment_start: Default::default(),
6118                    }));
6119                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6120            }
6121        }
6122        AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
6123            if let NodeMut::SketchBlock(sketch_block) = node {
6124                let empty_defined_names = HashSet::new();
6125                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6126                let Ok(name) = next_free_name(prefix, defined_names) else {
6127                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6128                };
6129                sketch_block
6130                    .body
6131                    .items
6132                    .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
6133                        ast::VariableDeclaration::new(
6134                            ast::VariableDeclarator::new(&name, expr.clone()),
6135                            ast::ItemVisibility::Default,
6136                            ast::VariableKind::Const,
6137                        ),
6138                    ))));
6139                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
6140            }
6141        }
6142        AstMutateCommand::AddVariableDeclaration { prefix } => {
6143            if let NodeMut::VariableDeclaration(inner) = node {
6144                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
6145            }
6146            if let NodeMut::ExpressionStatement(expr_stmt) = node {
6147                let empty_defined_names = HashSet::new();
6148                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6149                let Ok(name) = next_free_name(prefix, defined_names) else {
6150                    // TODO: Return an error instead?
6151                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6152                };
6153                let mutate_node =
6154                    ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(ast::VariableDeclaration::new(
6155                        ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
6156                        ast::ItemVisibility::Default,
6157                        ast::VariableKind::Const,
6158                    ))));
6159                return TraversalReturn {
6160                    mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
6161                    control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
6162                };
6163            }
6164        }
6165        AstMutateCommand::EditPoint { at } => {
6166            if let NodeMut::CallExpressionKw(call) = node {
6167                if call.callee.name.name != POINT_FN {
6168                    return TraversalReturn::new_continue(());
6169                }
6170                // Update the arguments.
6171                for labeled_arg in &mut call.arguments {
6172                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
6173                        labeled_arg.arg = at.clone();
6174                    }
6175                }
6176                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6177            }
6178        }
6179        AstMutateCommand::EditLine {
6180            start,
6181            end,
6182            construction,
6183        } => {
6184            if let NodeMut::CallExpressionKw(call) = node {
6185                if call.callee.name.name != LINE_FN {
6186                    return TraversalReturn::new_continue(());
6187                }
6188                // Update the arguments.
6189                for labeled_arg in &mut call.arguments {
6190                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
6191                        labeled_arg.arg = start.clone();
6192                    }
6193                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
6194                        labeled_arg.arg = end.clone();
6195                    }
6196                }
6197                // Handle construction kwarg
6198                if let Some(construction_value) = construction {
6199                    let construction_exists = call
6200                        .arguments
6201                        .iter()
6202                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6203                    if *construction_value {
6204                        // Add or update construction=true
6205                        if construction_exists {
6206                            // Update existing construction kwarg
6207                            for labeled_arg in &mut call.arguments {
6208                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6209                                    labeled_arg.arg =
6210                                        ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6211                                            value: ast::LiteralValue::Bool(true),
6212                                            raw: "true".to_string(),
6213                                            digest: None,
6214                                        })));
6215                                }
6216                            }
6217                        } else {
6218                            // Add new construction kwarg
6219                            call.arguments.push(ast::LabeledArg {
6220                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6221                                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6222                                    value: ast::LiteralValue::Bool(true),
6223                                    raw: "true".to_string(),
6224                                    digest: None,
6225                                }))),
6226                            });
6227                        }
6228                    } else {
6229                        // Remove construction kwarg if it exists
6230                        call.arguments
6231                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6232                    }
6233                }
6234                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6235            }
6236        }
6237        AstMutateCommand::EditArc {
6238            start,
6239            end,
6240            center,
6241            direction,
6242            construction,
6243        } => {
6244            if let NodeMut::CallExpressionKw(call) = node {
6245                if call.callee.name.name != ARC_FN {
6246                    return TraversalReturn::new_continue(());
6247                }
6248                // Update the arguments.
6249                for labeled_arg in &mut call.arguments {
6250                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
6251                        labeled_arg.arg = start.clone();
6252                    }
6253                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
6254                        labeled_arg.arg = end.clone();
6255                    }
6256                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
6257                        labeled_arg.arg = center.clone();
6258                    }
6259                }
6260                // Handle direction kwarg
6261                if let Some(direction_value) = direction {
6262                    let direction_exists = call
6263                        .arguments
6264                        .iter()
6265                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_DIRECTION_PARAM));
6266                    if direction_value.is_clockwise() {
6267                        let direction_ast = ast::Expr::Name(BoxNode::new(ast::Name::new(ARC_DIRECTION_CW_NAME)));
6268                        if direction_exists {
6269                            // Update existing direction kwarg
6270                            for labeled_arg in &mut call.arguments {
6271                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_DIRECTION_PARAM) {
6272                                    labeled_arg.arg = direction_ast.clone();
6273                                }
6274                            }
6275                        } else {
6276                            // Add new direction kwarg
6277                            call.arguments.push(ast::LabeledArg {
6278                                label: Some(ast::Identifier::new(ARC_DIRECTION_PARAM)),
6279                                arg: direction_ast,
6280                            });
6281                        }
6282                    } else {
6283                        // Remove direction kwarg if it exists since
6284                        // counterclockwise is the default
6285                        call.arguments
6286                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(ARC_DIRECTION_PARAM));
6287                    }
6288                }
6289                // Handle construction kwarg
6290                if let Some(construction_value) = construction {
6291                    let construction_exists = call
6292                        .arguments
6293                        .iter()
6294                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6295                    if *construction_value {
6296                        // Add or update construction=true
6297                        if construction_exists {
6298                            // Update existing construction kwarg
6299                            for labeled_arg in &mut call.arguments {
6300                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6301                                    labeled_arg.arg =
6302                                        ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6303                                            value: ast::LiteralValue::Bool(true),
6304                                            raw: "true".to_string(),
6305                                            digest: None,
6306                                        })));
6307                                }
6308                            }
6309                        } else {
6310                            // Add new construction kwarg
6311                            call.arguments.push(ast::LabeledArg {
6312                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6313                                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6314                                    value: ast::LiteralValue::Bool(true),
6315                                    raw: "true".to_string(),
6316                                    digest: None,
6317                                }))),
6318                            });
6319                        }
6320                    } else {
6321                        // Remove construction kwarg if it exists
6322                        call.arguments
6323                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6324                    }
6325                }
6326                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6327            }
6328        }
6329        AstMutateCommand::EditCircle {
6330            start,
6331            center,
6332            construction,
6333        } => {
6334            if let NodeMut::CallExpressionKw(call) = node {
6335                if call.callee.name.name != CIRCLE_FN {
6336                    return TraversalReturn::new_continue(());
6337                }
6338                // Update the arguments.
6339                for labeled_arg in &mut call.arguments {
6340                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
6341                        labeled_arg.arg = start.clone();
6342                    }
6343                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
6344                        labeled_arg.arg = center.clone();
6345                    }
6346                }
6347                // Handle construction kwarg
6348                if let Some(construction_value) = construction {
6349                    let construction_exists = call
6350                        .arguments
6351                        .iter()
6352                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6353                    if *construction_value {
6354                        if construction_exists {
6355                            // Update existing construction kwarg
6356                            for labeled_arg in &mut call.arguments {
6357                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6358                                    labeled_arg.arg =
6359                                        ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6360                                            value: ast::LiteralValue::Bool(true),
6361                                            raw: "true".to_string(),
6362                                            digest: None,
6363                                        })));
6364                                }
6365                            }
6366                        } else {
6367                            // Add new construction kwarg
6368                            call.arguments.push(ast::LabeledArg {
6369                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6370                                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6371                                    value: ast::LiteralValue::Bool(true),
6372                                    raw: "true".to_string(),
6373                                    digest: None,
6374                                }))),
6375                            });
6376                        }
6377                    } else {
6378                        // Remove construction kwarg if it exists
6379                        call.arguments
6380                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6381                    }
6382                }
6383                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6384            }
6385        }
6386        AstMutateCommand::EditControlPointSpline { points, construction } => {
6387            if let NodeMut::CallExpressionKw(call) = node {
6388                if call.callee.name.name != CONTROL_POINT_SPLINE_FN {
6389                    return TraversalReturn::new_continue(());
6390                }
6391                for labeled_arg in &mut call.arguments {
6392                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONTROL_POINT_SPLINE_POINTS_PARAM)
6393                    {
6394                        labeled_arg.arg = points.clone();
6395                    }
6396                }
6397                // Handle construction kwarg
6398                if let Some(construction_value) = construction {
6399                    let construction_exists = call
6400                        .arguments
6401                        .iter()
6402                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6403                    if *construction_value {
6404                        if construction_exists {
6405                            for labeled_arg in &mut call.arguments {
6406                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6407                                    labeled_arg.arg =
6408                                        ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6409                                            value: ast::LiteralValue::Bool(true),
6410                                            raw: "true".to_string(),
6411                                            digest: None,
6412                                        })));
6413                                }
6414                            }
6415                        } else {
6416                            call.arguments.push(ast::LabeledArg {
6417                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6418                                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6419                                    value: ast::LiteralValue::Bool(true),
6420                                    raw: "true".to_string(),
6421                                    digest: None,
6422                                }))),
6423                            });
6424                        }
6425                    } else {
6426                        call.arguments
6427                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6428                    }
6429                }
6430                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6431            }
6432        }
6433        AstMutateCommand::EditConstraintValue { value } => {
6434            if let NodeMut::BinaryExpression(binary_expr) = node {
6435                let left_is_constraint = matches!(
6436                    &binary_expr.left,
6437                    ast::BinaryPart::CallExpressionKw(call) if is_constraint_call_name(call.callee.name.name.as_str())
6438                );
6439                if left_is_constraint {
6440                    binary_expr.right = value.clone();
6441                } else {
6442                    binary_expr.left = value.clone();
6443                }
6444
6445                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6446            }
6447        }
6448        AstMutateCommand::EditAngleConstraint { call, value } => {
6449            if let NodeMut::BinaryExpression(binary_expr) = node {
6450                let left_is_angle = matches!(
6451                    &binary_expr.left,
6452                    ast::BinaryPart::CallExpressionKw(existing_call)
6453                        if is_angle_constraint_call_name(existing_call.callee.name.name.as_str())
6454                );
6455                let right_is_angle = matches!(
6456                    &binary_expr.right,
6457                    ast::BinaryPart::CallExpressionKw(existing_call)
6458                        if is_angle_constraint_call_name(existing_call.callee.name.name.as_str())
6459                );
6460
6461                match (left_is_angle, right_is_angle) {
6462                    (true, _) => {
6463                        binary_expr.left = call.clone();
6464                        binary_expr.right = value.clone();
6465                    }
6466                    (false, true) => {
6467                        binary_expr.left = value.clone();
6468                        binary_expr.right = call.clone();
6469                    }
6470                    (false, false) => return TraversalReturn::new_continue(()),
6471                }
6472
6473                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6474            }
6475        }
6476        AstMutateCommand::EditDistanceConstraint { call, value } => {
6477            if let NodeMut::BinaryExpression(binary_expr) = node {
6478                let left_is_distance = matches!(
6479                    &binary_expr.left,
6480                    ast::BinaryPart::CallExpressionKw(existing_call)
6481                        if is_distance_constraint_call_name(existing_call.callee.name.name.as_str())
6482                );
6483                let right_is_distance = matches!(
6484                    &binary_expr.right,
6485                    ast::BinaryPart::CallExpressionKw(existing_call)
6486                        if is_distance_constraint_call_name(existing_call.callee.name.name.as_str())
6487                );
6488
6489                match (left_is_distance, right_is_distance) {
6490                    (true, _) => {
6491                        binary_expr.left = call.clone();
6492                        binary_expr.right = value.clone();
6493                    }
6494                    (false, true) => {
6495                        binary_expr.left = value.clone();
6496                        binary_expr.right = call.clone();
6497                    }
6498                    (false, false) => return TraversalReturn::new_continue(()),
6499                }
6500
6501                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6502            }
6503        }
6504        AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
6505            if let NodeMut::BinaryExpression(binary_expr) = node {
6506                let call = if let Some(call) = constraint_supports_label_position(&mut binary_expr.left) {
6507                    call
6508                } else if let Some(call) = constraint_supports_label_position(&mut binary_expr.right) {
6509                    call
6510                } else {
6511                    return TraversalReturn::new_continue(());
6512                };
6513
6514                if let Some(label_arg) = call
6515                    .arguments
6516                    .iter_mut()
6517                    .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
6518                {
6519                    label_arg.arg = label_position.clone();
6520                } else {
6521                    call.arguments.push(ast::LabeledArg {
6522                        label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
6523                        arg: label_position.clone(),
6524                    });
6525                }
6526
6527                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6528            }
6529        }
6530        AstMutateCommand::EditCallUnlabeled { arg } => {
6531            if let NodeMut::CallExpressionKw(call) = node {
6532                call.unlabeled = Some(arg.clone());
6533                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6534            }
6535        }
6536        AstMutateCommand::EditVarInitialValue { value } => {
6537            // We target the SketchVar itself (matched by NodePath) rather than
6538            // the inner NumericLiteral so we can also write back into vars that
6539            // were declared without an initial value (e.g. bare `var`).
6540            if let NodeMut::SketchVar(sketch_var) = node {
6541                let Ok(literal) = to_source_number(*value) else {
6542                    return TraversalReturn::new_break(Err(KclError::refactor(format!(
6543                        "Could not convert number to AST literal: {:?}",
6544                        *value
6545                    ))));
6546                };
6547                sketch_var.initial = Some(BoxNode::new(ast::Node::no_src(literal)));
6548                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6549            }
6550        }
6551        AstMutateCommand::DeleteNode => {
6552            return TraversalReturn {
6553                mutate_body_item: MutateBodyItem::Delete,
6554                control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
6555            };
6556        }
6557    }
6558    TraversalReturn::new_continue(())
6559}
6560
6561struct FindSketchBlockSourceRange {
6562    /// The source range of the sketch block before mutation.
6563    target_before_mutation: SourceRange,
6564    /// The source range of the sketch block's last body item after mutation. We
6565    /// need to use a [Cell] since the [crate::walk::Visitor] trait requires a
6566    /// shared reference.
6567    found: Cell<Option<AstNodeRef>>,
6568}
6569
6570impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
6571    type Error = crate::front::Error;
6572
6573    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6574        let Ok(node_range) = SourceRange::try_from(&node) else {
6575            return Ok(true);
6576        };
6577
6578        if let crate::walk::Node::SketchBlock(sketch_block) = node {
6579            if node_range.module_id() == self.target_before_mutation.module_id()
6580                && node_range.start() == self.target_before_mutation.start()
6581                // End shouldn't match since we added something.
6582                && node_range.end() >= self.target_before_mutation.end()
6583            {
6584                self.found.set(sketch_block.body.items.last().map(|item| match item {
6585                    // For declarations like `circle1 = circle(...)`, use
6586                    // the init expression range so lookup in source_range_to_object
6587                    // matches the segment source range.
6588                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6589                    _ => AstNodeRef::from(item),
6590                }));
6591                return Ok(false);
6592            } else {
6593                // We found a different sketch block. No need to descend into
6594                // its children since sketch blocks cannot be nested.
6595                return Ok(true);
6596            }
6597        }
6598
6599        for child in node.children().iter() {
6600            if !child.visit(*self)? {
6601                return Ok(false);
6602            }
6603        }
6604
6605        Ok(true)
6606    }
6607}
6608
6609struct FindSketchBlockByNodePath {
6610    /// The Node Path of the sketch block before mutation.
6611    target_node_path: ast::NodePath,
6612    /// The ref of the sketch block's last body item after mutation. We need to
6613    /// use a [Cell] since the [crate::walk::Visitor] trait requires a shared
6614    /// reference.
6615    found: Cell<Option<AstNodeRef>>,
6616}
6617
6618impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
6619    type Error = crate::front::Error;
6620
6621    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6622        let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
6623            return Ok(true);
6624        };
6625
6626        if let crate::walk::Node::SketchBlock(sketch_block) = node {
6627            if let Some(node_path) = node_path
6628                && node_path == self.target_node_path
6629            {
6630                self.found.set(sketch_block.body.items.last().map(|item| match item {
6631                    // For declarations like `circle1 = circle(...)`, use
6632                    // the init expression range so lookup in source_range_to_object
6633                    // matches the segment source range.
6634                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6635                    _ => AstNodeRef::from(item),
6636                }));
6637
6638                return Ok(false);
6639            } else {
6640                // We found a different sketch block. No need to descend into
6641                // its children since sketch blocks cannot be nested.
6642                return Ok(true);
6643            }
6644        }
6645
6646        for child in node.children().iter() {
6647            if !child.visit(*self)? {
6648                return Ok(false);
6649            }
6650        }
6651
6652        Ok(true)
6653    }
6654}
6655
6656/// After adding an item to a sketch block, find the sketch block, and get the
6657/// source range of the added item. We assume that the added item is the last
6658/// item in the sketch block and that the sketch block's source range has grown,
6659/// but not moved from its starting offset.
6660///
6661/// TODO: Do we need to format *before* mutation in case formatting moves the
6662/// sketch block forward?
6663fn find_sketch_block_added_item(
6664    ast: &ast::Node<ast::Program>,
6665    sketch_block_before_mutation: &AstNodeRef,
6666) -> Result<AstNodeRef, KclError> {
6667    if let Some(node_path) = &sketch_block_before_mutation.node_path {
6668        let find = FindSketchBlockByNodePath {
6669            target_node_path: node_path.clone(),
6670            found: Cell::new(None),
6671        };
6672        let node = crate::walk::Node::from(ast);
6673        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6674        find.found.into_inner().ok_or_else(|| {
6675            KclError::refactor(format!(
6676                "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
6677            ))
6678        })
6679    } else {
6680        // No NodePath. Fall back to legacy source range.
6681        let find = FindSketchBlockSourceRange {
6682            target_before_mutation: sketch_block_before_mutation.range,
6683            found: Cell::new(None),
6684        };
6685        let node = crate::walk::Node::from(ast);
6686        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6687        find.found.into_inner().ok_or_else(|| KclError::refactor(
6688            format!("Source range after mutation not found for range before mutation: {sketch_block_before_mutation:?}; Did you try formatting (i.e. call recast) before calling this?"),
6689        ))
6690    }
6691}
6692
6693fn format_kcl_error_message(prefix: &str, error: &KclError) -> String {
6694    let message = error.message().trim();
6695    let message = if message.is_empty() {
6696        "unknown parse error"
6697    } else {
6698        message
6699    };
6700
6701    format!("{prefix}: {message}")
6702}
6703
6704fn parse_frontend_mutation_source(source: &str, parse_error_prefix: &str, no_ast_message: &str) -> ExecResult<Program> {
6705    let (program, errors) = Program::parse(source).map_err(|err| {
6706        KclErrorWithOutputs::no_outputs(KclError::refactor(format_kcl_error_message(parse_error_prefix, &err)))
6707    })?;
6708    if !errors.is_empty() {
6709        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
6710            format_compilation_issues(parse_error_prefix, &errors),
6711        )));
6712    }
6713
6714    program.ok_or_else(|| KclErrorWithOutputs::no_outputs(KclError::refactor(no_ast_message.to_owned())))
6715}
6716
6717fn format_compilation_issues(prefix: &str, issues: &[CompilationIssue]) -> String {
6718    let Some(first_issue) = issues
6719        .iter()
6720        .find(|issue| issue.severity.is_err())
6721        .or_else(|| issues.first())
6722    else {
6723        return prefix.to_owned();
6724    };
6725
6726    let message = first_issue.message.trim();
6727    let message = if message.is_empty() {
6728        "unknown parse error"
6729    } else {
6730        message
6731    };
6732
6733    if issues.len() > 1 {
6734        format!("{prefix}: {message} (+{} more)", issues.len() - 1)
6735    } else {
6736        format!("{prefix}: {message}")
6737    }
6738}
6739
6740fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
6741    // TODO: Don't duplicate this from lib.rs Program.
6742    ast.recast_top(&Default::default(), 0)
6743}
6744
6745struct FindNumericLiteral {
6746    target: SourceRange,
6747    found: Cell<Option<ast::NumericLiteral>>,
6748}
6749
6750impl<'a> crate::walk::Visitor<'a> for &FindNumericLiteral {
6751    type Error = crate::front::Error;
6752
6753    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6754        let Ok(node_range) = SourceRange::try_from(&node) else {
6755            return Ok(true);
6756        };
6757
6758        if node_range == self.target
6759            && let crate::walk::Node::NumericLiteral(literal) = node
6760        {
6761            self.found.set(Some(literal.inner.clone()));
6762            return Ok(false);
6763        }
6764
6765        for child in node.children().iter() {
6766            if !child.visit(*self)? {
6767                return Ok(false);
6768            }
6769        }
6770
6771        Ok(true)
6772    }
6773}
6774
6775fn numeric_literal_at_source_range(ast: &ast::Node<ast::Program>, target: SourceRange) -> Option<ast::NumericLiteral> {
6776    let find = FindNumericLiteral {
6777        target,
6778        found: Cell::new(None),
6779    };
6780    let node = crate::walk::Node::from(ast);
6781    node.visit(&find).ok()?;
6782    find.found.into_inner()
6783}
6784
6785struct FindSketchVarInitialByNodePath<'a> {
6786    target: &'a ast::NodePath,
6787    sketch_var_found: Cell<bool>,
6788    initial_literal: Cell<Option<ast::NumericLiteral>>,
6789}
6790
6791impl<'a, 'b> crate::walk::Visitor<'b> for &FindSketchVarInitialByNodePath<'a> {
6792    type Error = crate::front::Error;
6793
6794    fn visit_node(&self, node: crate::walk::Node<'b>) -> anyhow::Result<bool, Self::Error> {
6795        if let crate::walk::Node::SketchVar(sketch_var) = node
6796            && sketch_var.node_path.as_ref() == Some(self.target)
6797        {
6798            self.sketch_var_found.set(true);
6799            if let Some(initial) = &sketch_var.initial {
6800                self.initial_literal.set(Some(initial.inner.clone()));
6801            }
6802            return Ok(false);
6803        }
6804
6805        for child in node.children().iter() {
6806            if !child.visit(*self)? {
6807                return Ok(false);
6808            }
6809        }
6810
6811        Ok(true)
6812    }
6813}
6814
6815/// Locate the source `var` declaration corresponding to a sketch-var solution.
6816///
6817/// The outer [`Option`] distinguishes "no matching target" (commit must fail)
6818/// from "target found." The inner [`Option`] is the initial numeric literal of
6819/// the [`SketchVar`], if any; bare `var` declarations return `Some(None)`.
6820///
6821/// When `node_path` is `None` (e.g. for older outcomes that predate the
6822/// node-path propagation), this falls back to source-range matching, which
6823/// can break under whitespace shifts elsewhere in the file.
6824fn numeric_literal_at_node_path(
6825    ast: &ast::Node<ast::Program>,
6826    node_path: Option<&ast::NodePath>,
6827    source_range: SourceRange,
6828) -> Option<Option<ast::NumericLiteral>> {
6829    let Some(node_path) = node_path else {
6830        let message = "numeric_literal_at_node_path: missing node_path on var solution; falling back to source-range lookup, which can fail under whitespace shifts";
6831        #[cfg(target_arch = "wasm32")]
6832        web_sys::console::warn_1(&message.into());
6833        #[cfg(not(target_arch = "wasm32"))]
6834        eprintln!("WARNING: {message}");
6835        return numeric_literal_at_source_range(ast, source_range).map(Some);
6836    };
6837    let find = FindSketchVarInitialByNodePath {
6838        target: node_path,
6839        sketch_var_found: Cell::new(false),
6840        initial_literal: Cell::new(None),
6841    };
6842    let node = crate::walk::Node::from(ast);
6843    node.visit(&find).ok()?;
6844    if !find.sketch_var_found.get() {
6845        return None;
6846    }
6847    Some(find.initial_literal.into_inner())
6848}
6849
6850fn suffix_length_unit(suffix: NumericSuffix) -> Option<UnitLength> {
6851    match suffix {
6852        NumericSuffix::Mm => Some(UnitLength::Millimeters),
6853        NumericSuffix::Cm => Some(UnitLength::Centimeters),
6854        NumericSuffix::M => Some(UnitLength::Meters),
6855        NumericSuffix::Inch => Some(UnitLength::Inches),
6856        NumericSuffix::Ft => Some(UnitLength::Feet),
6857        NumericSuffix::Yd => Some(UnitLength::Yards),
6858        _ => None,
6859    }
6860}
6861
6862fn number_value_in_default_length_units(number: Number, default_length_unit: UnitLength) -> f64 {
6863    match suffix_length_unit(number.units) {
6864        Some(unit) => adjust_length(unit, number.value, default_length_unit).0,
6865        None => number.value,
6866    }
6867}
6868
6869fn literal_value_in_default_length_units(literal: &ast::NumericLiteral, default_length_unit: UnitLength) -> f64 {
6870    match suffix_length_unit(literal.suffix) {
6871        Some(unit) => adjust_length(unit, literal.value, default_length_unit).0,
6872        None => literal.value,
6873    }
6874}
6875
6876fn var_solution_needs_commit(
6877    current_literal: &ast::NumericLiteral,
6878    solved_value: Number,
6879    default_length_unit: UnitLength,
6880) -> bool {
6881    let current = literal_value_in_default_length_units(current_literal, default_length_unit);
6882    let solved = number_value_in_default_length_units(solved_value, default_length_unit);
6883
6884    (current - solved).abs() > 1e-9
6885}
6886
6887fn preserve_var_solution_literal_style(
6888    current_literal: &ast::NumericLiteral,
6889    solved_value: Number,
6890    default_length_unit: UnitLength,
6891) -> Number {
6892    if current_literal.suffix == NumericSuffix::None {
6893        return Number {
6894            value: number_value_in_default_length_units(solved_value, default_length_unit),
6895            units: NumericSuffix::None,
6896        };
6897    }
6898
6899    let Some(current_unit) = suffix_length_unit(current_literal.suffix) else {
6900        return solved_value;
6901    };
6902
6903    let solved_default_value = number_value_in_default_length_units(solved_value, default_length_unit);
6904    Number {
6905        value: adjust_length(default_length_unit, solved_default_value, current_unit).0,
6906        units: current_literal.suffix,
6907    }
6908}
6909
6910pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
6911    Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node {
6912        inner: ast::ArrayExpression {
6913            elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
6914            non_code_meta: Default::default(),
6915            digest: None,
6916        },
6917        start: Default::default(),
6918        end: Default::default(),
6919        module_id: Default::default(),
6920        node_path: None,
6921        outer_attrs: Default::default(),
6922        pre_comments: Default::default(),
6923        comment_start: Default::default(),
6924    })))
6925}
6926
6927pub(crate) fn to_ast_point2d_array(points: &[Point2d<Expr>]) -> anyhow::Result<ast::Expr> {
6928    Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
6929        ast::ArrayExpression {
6930            elements: points.iter().map(to_ast_point2d).collect::<anyhow::Result<Vec<_>>>()?,
6931            digest: None,
6932            non_code_meta: Default::default(),
6933        },
6934    ))))
6935}
6936
6937fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
6938    Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
6939        ast::ArrayExpression {
6940            elements: vec![
6941                ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6942                    point.x,
6943                )?)))),
6944                ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6945                    point.y,
6946                )?)))),
6947            ],
6948            non_code_meta: Default::default(),
6949            digest: None,
6950        },
6951    ))))
6952}
6953
6954fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
6955    match expr {
6956        Expr::Number(number) => Ok(ast::Expr::Literal(BoxNode::new(ast::Node {
6957            inner: ast::Literal::from(to_source_number(*number)?),
6958            start: Default::default(),
6959            end: Default::default(),
6960            module_id: Default::default(),
6961            node_path: None,
6962            outer_attrs: Default::default(),
6963            pre_comments: Default::default(),
6964            comment_start: Default::default(),
6965        }))),
6966        Expr::Var(number) => Ok(ast::Expr::SketchVar(BoxNode::new(ast::Node {
6967            inner: ast::SketchVar {
6968                initial: Some(BoxNode::new(ast::Node {
6969                    inner: to_source_number(*number)?,
6970                    start: Default::default(),
6971                    end: Default::default(),
6972                    module_id: Default::default(),
6973                    node_path: None,
6974                    outer_attrs: Default::default(),
6975                    pre_comments: Default::default(),
6976                    comment_start: Default::default(),
6977                })),
6978                digest: None,
6979            },
6980            start: Default::default(),
6981            end: Default::default(),
6982            module_id: Default::default(),
6983            node_path: None,
6984            outer_attrs: Default::default(),
6985            pre_comments: Default::default(),
6986            comment_start: Default::default(),
6987        }))),
6988        Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
6989    }
6990}
6991
6992fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
6993    Ok(ast::NumericLiteral {
6994        value: number.value,
6995        suffix: number.units,
6996        raw: format_number_literal(number.value, number.units, None)?,
6997        digest: None,
6998    })
6999}
7000
7001pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
7002    ast::Expr::Name(BoxNode::new(ast_name(name)))
7003}
7004
7005fn ast_name(name: String) -> ast::Node<ast::Name> {
7006    ast::Node {
7007        inner: ast::Name {
7008            name: ast::Node {
7009                inner: ast::Identifier { name, digest: None },
7010                start: Default::default(),
7011                end: Default::default(),
7012                module_id: Default::default(),
7013                node_path: None,
7014                outer_attrs: Default::default(),
7015                pre_comments: Default::default(),
7016                comment_start: Default::default(),
7017            },
7018            path: Vec::new(),
7019            abs_path: false,
7020            digest: None,
7021        },
7022        start: Default::default(),
7023        end: Default::default(),
7024        module_id: Default::default(),
7025        node_path: None,
7026        outer_attrs: Default::default(),
7027        pre_comments: Default::default(),
7028        comment_start: Default::default(),
7029    }
7030}
7031
7032pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
7033    ast::Name {
7034        name: ast::Node {
7035            inner: ast::Identifier {
7036                name: name.to_owned(),
7037                digest: None,
7038            },
7039            start: Default::default(),
7040            end: Default::default(),
7041            module_id: Default::default(),
7042            node_path: None,
7043            outer_attrs: Default::default(),
7044            pre_comments: Default::default(),
7045            comment_start: Default::default(),
7046        },
7047        path: Default::default(),
7048        abs_path: false,
7049        digest: None,
7050    }
7051}
7052
7053/// 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 horizontal(line)
7076pub(crate) fn create_horizontal_ast(line_expr: 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(HORIZONTAL_FN)),
7079        unlabeled: Some(line_expr),
7080        arguments: Default::default(),
7081        digest: None,
7082        non_code_meta: Default::default(),
7083    })))
7084}
7085
7086/// Create an AST node for vertical(line)
7087pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
7088    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7089        callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
7090        unlabeled: Some(line_expr),
7091        arguments: Default::default(),
7092        digest: None,
7093        non_code_meta: Default::default(),
7094    })))
7095}
7096
7097/// Create a member expression like object.property (e.g., line1.end)
7098pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
7099    ast::Expr::MemberExpression(BoxNode::new(ast::Node::no_src(ast::MemberExpression {
7100        object: object_expr,
7101        property: ast::Expr::Name(BoxNode::new(ast::Node::no_src(ast::Name {
7102            name: ast::Node::no_src(ast::Identifier {
7103                name: property.to_string(),
7104                digest: None,
7105            }),
7106            path: Vec::new(),
7107            abs_path: false,
7108            digest: None,
7109        }))),
7110        computed: false,
7111        digest: None,
7112    })))
7113}
7114
7115pub(crate) fn create_index_expression(object_expr: ast::Expr, index: usize) -> ast::Expr {
7116    ast::Expr::MemberExpression(BoxNode::new(ast::Node::no_src(ast::MemberExpression {
7117        object: object_expr,
7118        property: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(
7119            ast::NumericLiteral {
7120                value: index as f64,
7121                suffix: NumericSuffix::None,
7122                raw: index.to_string(),
7123                digest: None,
7124            },
7125        )))),
7126        computed: true,
7127        digest: None,
7128    })))
7129}
7130
7131/// Create an AST node for `fixed([point, [x, y]])`.
7132fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
7133    // Create [x, y] array literal.
7134    let x_literal = ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
7135        position.x,
7136    )?))));
7137    let y_literal = ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
7138        position.y,
7139    )?))));
7140    let point_array = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7141        elements: vec![x_literal, y_literal],
7142        digest: None,
7143        non_code_meta: Default::default(),
7144    })));
7145
7146    // Create [point, [x, y]] outer array.
7147    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7148        elements: vec![point_expr, point_array],
7149        digest: None,
7150        non_code_meta: Default::default(),
7151    })));
7152
7153    // Create fixed([...])
7154    Ok(ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(
7155        ast::CallExpressionKw {
7156            callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
7157            unlabeled: Some(array_expr),
7158            arguments: Default::default(),
7159            digest: None,
7160            non_code_meta: Default::default(),
7161        },
7162    ))))
7163}
7164
7165/// Create an AST node for equalLength([line1, line2, ...])
7166pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
7167    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7168        elements: line_exprs,
7169        digest: None,
7170        non_code_meta: Default::default(),
7171    })));
7172
7173    // Create equalLength([...])
7174    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7175        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
7176        unlabeled: Some(array_expr),
7177        arguments: Default::default(),
7178        digest: None,
7179        non_code_meta: Default::default(),
7180    })))
7181}
7182
7183/// Create an AST node for equalRadius([seg1, seg2, ...])
7184pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
7185    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7186        elements: segment_exprs,
7187        digest: None,
7188        non_code_meta: Default::default(),
7189    })));
7190
7191    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7192        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
7193        unlabeled: Some(array_expr),
7194        arguments: Default::default(),
7195        digest: None,
7196        non_code_meta: Default::default(),
7197    })))
7198}
7199
7200/// Create an AST node for tangent([seg1, seg2])
7201pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
7202    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7203        elements: vec![seg1_expr, seg2_expr],
7204        digest: None,
7205        non_code_meta: Default::default(),
7206    })));
7207
7208    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7209        callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
7210        unlabeled: Some(array_expr),
7211        arguments: Default::default(),
7212        digest: None,
7213        non_code_meta: Default::default(),
7214    })))
7215}
7216
7217/// Create an AST node for symmetric([input1, input2], axis = line)
7218pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
7219    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7220        elements: input_exprs,
7221        digest: None,
7222        non_code_meta: Default::default(),
7223    })));
7224    let arguments = vec![ast::LabeledArg {
7225        label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
7226        arg: axis_expr,
7227    }];
7228
7229    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7230        callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
7231        unlabeled: Some(array_expr),
7232        arguments,
7233        digest: None,
7234        non_code_meta: Default::default(),
7235    })))
7236}
7237
7238/// Create an AST node for midpoint(segment, point = point)
7239pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
7240    let arguments = vec![ast::LabeledArg {
7241        label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
7242        arg: point_expr,
7243    }];
7244
7245    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7246        callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
7247        unlabeled: Some(segment_expr),
7248        arguments,
7249        digest: None,
7250        non_code_meta: Default::default(),
7251    })))
7252}
7253
7254/// The source range to attach to a diagnostic for `error` in the top-level
7255/// module. Source ranges are ordered innermost first and may point into
7256/// imported modules, so prefer the innermost range that is in the top-level
7257/// module; otherwise fall back to the innermost range.
7258fn issue_source_range(error: &KclError) -> SourceRange {
7259    let source_ranges = error.source_ranges();
7260    source_ranges
7261        .iter()
7262        .find(|range| range.is_top_level_module())
7263        .or_else(|| source_ranges.first())
7264        .copied()
7265        .unwrap_or_else(SourceRange::synthetic)
7266}
7267
7268#[cfg(test)]
7269mod tests {
7270    use std::sync;
7271
7272    use super::*;
7273    use crate::engine::PlaneName;
7274    use crate::engine::engine_manager::EngineManager;
7275    use crate::execution::cache::SketchModeState;
7276    use crate::execution::cache::clear_mem_cache;
7277    use crate::execution::cache::read_old_memory;
7278    use crate::execution::cache::write_old_memory;
7279    use crate::front::Distance;
7280    use crate::front::Fixed;
7281    use crate::front::FixedPoint;
7282    use crate::front::Midpoint;
7283    use crate::front::Object;
7284    use crate::front::Plane;
7285    use crate::front::Sketch;
7286    use crate::front::Tangent;
7287    use crate::frontend::sketch::Vertical;
7288    use crate::pretty::NumericSuffix;
7289
7290    fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
7291        for object in &scene_graph.objects {
7292            if let ObjectKind::Sketch(_) = &object.kind {
7293                return Some(object);
7294            }
7295        }
7296        None
7297    }
7298
7299    fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
7300        for object in &scene_graph.objects {
7301            if let ObjectKind::Face(_) = &object.kind {
7302                return Some(object);
7303            }
7304        }
7305        None
7306    }
7307
7308    fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
7309        for object in &scene_graph.objects {
7310            if matches!(&object.kind, ObjectKind::Wall(_)) {
7311                return Some(object.id);
7312            }
7313        }
7314        None
7315    }
7316
7317    fn find_cap_object_id_with_solid_output_index(
7318        scene_graph: &SceneGraph,
7319        cap_kind: crate::frontend::api::CapKind,
7320        solid_output_index: usize,
7321    ) -> Option<ObjectId> {
7322        for object in &scene_graph.objects {
7323            if matches!(&object.kind, ObjectKind::Cap(cap) if cap.kind == cap_kind && cap.solid_output_index == Some(solid_output_index))
7324            {
7325                return Some(object.id);
7326            }
7327        }
7328        None
7329    }
7330
7331    #[test]
7332    fn issue_source_range_prefers_top_level_module() {
7333        use kcl_error::ModuleId;
7334
7335        let top = SourceRange::new(10, 20, ModuleId::default());
7336        let imported = SourceRange::new(0, 5, ModuleId::from_usize(7));
7337
7338        // Innermost frame is in an imported module; the diagnostic should
7339        // land on the innermost top-level range instead.
7340        let error = KclError::new_semantic(crate::errors::KclErrorDetails::new(
7341            "boom".to_owned(),
7342            vec![imported, top],
7343        ));
7344        assert_eq!(super::issue_source_range(&error), top);
7345
7346        // No top-level range at all: fall back to the innermost one.
7347        let error = KclError::new_semantic(crate::errors::KclErrorDetails::new("boom".to_owned(), vec![imported]));
7348        assert_eq!(super::issue_source_range(&error), imported);
7349
7350        // No ranges: synthetic.
7351        let error = KclError::new_semantic(crate::errors::KclErrorDetails::new("boom".to_owned(), vec![]));
7352        assert_eq!(super::issue_source_range(&error), SourceRange::synthetic());
7353    }
7354
7355    #[test]
7356    fn composite_constituent_sweeps_are_not_solid_outputs() {
7357        use kcl_api::artifact::ArtifactSweepMethod;
7358        use kcl_api::artifact::CompositeSolid;
7359        use kcl_api::artifact::CompositeSolidSubType;
7360        use kcl_api::artifact::Sweep;
7361        use kcl_api::artifact::SweepSubType;
7362
7363        let first_sweep_id = ArtifactId::new(Uuid::new_v4());
7364        let second_sweep_id = ArtifactId::new(Uuid::new_v4());
7365        let composite_id = ArtifactId::new(Uuid::new_v4());
7366        let code_ref = CodeRef::placeholder(SourceRange::synthetic());
7367        let sweep = |id| {
7368            Artifact::Sweep(Sweep {
7369                id,
7370                sub_type: SweepSubType::Extrusion,
7371                path_id: ArtifactId::new(Uuid::new_v4()),
7372                surface_ids: Vec::new(),
7373                edge_ids: Vec::new(),
7374                code_ref: code_ref.clone(),
7375                source_sweep_id: None,
7376                trajectory_id: None,
7377                method: ArtifactSweepMethod::New,
7378                consumed: false,
7379                pattern_ids: Vec::new(),
7380            })
7381        };
7382        let mut artifacts = IndexMap::from([
7383            (first_sweep_id, sweep(first_sweep_id)),
7384            (second_sweep_id, sweep(second_sweep_id)),
7385        ]);
7386
7387        let top_level_graph = ArtifactGraph::from_parts(artifacts.clone(), artifacts.len());
7388        assert_eq!(
7389            solid_output_index_for_sweep(&top_level_graph, first_sweep_id, &code_ref),
7390            Some(0)
7391        );
7392        assert_eq!(
7393            solid_output_index_for_sweep(&top_level_graph, second_sweep_id, &code_ref),
7394            Some(1)
7395        );
7396
7397        artifacts.insert(
7398            composite_id,
7399            Artifact::CompositeSolid(CompositeSolid {
7400                id: composite_id,
7401                consumed: false,
7402                sub_type: CompositeSolidSubType::Union,
7403                output_index: None,
7404                solid_ids: vec![first_sweep_id, second_sweep_id],
7405                tool_ids: Vec::new(),
7406                code_ref,
7407                composite_solid_id: None,
7408                pattern_ids: Vec::new(),
7409            }),
7410        );
7411        let composite_graph = ArtifactGraph::from_parts(artifacts.clone(), artifacts.len());
7412        assert_eq!(
7413            solid_output_index_for_sweep(&composite_graph, first_sweep_id, &CodeRef::default()),
7414            None
7415        );
7416        assert_eq!(
7417            solid_output_index_for_sweep(&composite_graph, second_sweep_id, &CodeRef::default()),
7418            None
7419        );
7420    }
7421
7422    #[test]
7423    fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
7424        let source = "\
7425region001 = region(point = [0.1, 0.1], sketch = s)
7426extrude001 = extrude(region001, length = 5)
7427revolve001 = revolve(region001, axis = Y)
7428sweep001 = sweep(region001, path = path001)
7429loft001 = loft(region001)
7430not_sweep001 = shell(extrude001, faces = [], thickness = 1)
7431";
7432
7433        let program = Program::parse(source).unwrap().0.unwrap();
7434
7435        assert_eq!(
7436            region_name_from_sweep_variable(&program.ast, "extrude001"),
7437            Some("region001".to_owned())
7438        );
7439        assert_eq!(
7440            region_name_from_sweep_variable(&program.ast, "revolve001"),
7441            Some("region001".to_owned())
7442        );
7443        assert_eq!(
7444            region_name_from_sweep_variable(&program.ast, "sweep001"),
7445            Some("region001".to_owned())
7446        );
7447        assert_eq!(
7448            region_name_from_sweep_variable(&program.ast, "loft001"),
7449            Some("region001".to_owned())
7450        );
7451        assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
7452    }
7453
7454    #[track_caller]
7455    fn expect_sketch(object: &Object) -> &Sketch {
7456        if let ObjectKind::Sketch(sketch) = &object.kind {
7457            sketch
7458        } else {
7459            panic!("Object is not a sketch: {:?}", object);
7460        }
7461    }
7462
7463    fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
7464        let point_object = scene_graph.objects.get(point_id.0).unwrap();
7465        let ObjectKind::Segment {
7466            segment: Segment::Point(point),
7467        } = &point_object.kind
7468        else {
7469            panic!("Object is not a point segment: {point_object:?}");
7470        };
7471        point.position.clone()
7472    }
7473
7474    fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
7475        assert!((actual.x.value - expected.x.value).abs() < 1e-6);
7476        assert!((actual.y.value - expected.y.value).abs() < 1e-6);
7477    }
7478
7479    /// Build a millimeter-valued point expression for concise sketch edit test
7480    /// setup.
7481    fn point_expr_mm(x: f64, y: f64) -> Point2d<Expr> {
7482        Point2d {
7483            x: Expr::Var(Number {
7484                value: x,
7485                units: NumericSuffix::Mm,
7486            }),
7487            y: Expr::Var(Number {
7488                value: y,
7489                units: NumericSuffix::Mm,
7490            }),
7491        }
7492    }
7493
7494    /// Build a millimeter-valued numeric point for comparing solved scene graph
7495    /// positions.
7496    fn point_number_mm(x: f64, y: f64) -> Point2d<Number> {
7497        Point2d {
7498            x: Number {
7499                value: x,
7500                units: NumericSuffix::Mm,
7501            },
7502            y: Number {
7503                value: y,
7504                units: NumericSuffix::Mm,
7505            },
7506        }
7507    }
7508
7509    fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
7510        LineCtor {
7511            start: Point2d {
7512                x: Expr::Number(Number { value: start_x, units }),
7513                y: Expr::Number(Number { value: start_y, units }),
7514            },
7515            end: Point2d {
7516                x: Expr::Number(Number { value: end_x, units }),
7517                y: Expr::Number(Number { value: end_y, units }),
7518            },
7519            construction: None,
7520        }
7521    }
7522
7523    async fn create_sketch_with_single_line(
7524        frontend: &mut FrontendState,
7525        ctx: &ExecutorContext,
7526        mock_ctx: &ExecutorContext,
7527        version: Version,
7528    ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
7529        frontend.program = Program::empty();
7530
7531        let sketch_args = SketchCtor {
7532            on: Plane::Default(PlaneName::Xy),
7533        };
7534        let (_src_delta, _scene_delta, sketch_id) = frontend
7535            .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
7536            .await
7537            .unwrap();
7538
7539        let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
7540        let (source_delta, scene_graph_delta) = frontend
7541            .add_segment(mock_ctx, version, sketch_id, segment, None)
7542            .await
7543            .unwrap();
7544        let line_id = *scene_graph_delta
7545            .new_objects
7546            .last()
7547            .expect("Expected line object id to be created");
7548
7549        (sketch_id, line_id, source_delta, scene_graph_delta)
7550    }
7551
7552    async fn seed_frontend_with_mock(frontend: &mut FrontendState, mock_ctx: &ExecutorContext, program: &Program) {
7553        frontend.program = program.clone();
7554        let outcome = mock_ctx.run_mock(program, &MockConfig::default()).await.unwrap();
7555        frontend.update_state_after_exec(outcome, true);
7556    }
7557
7558    #[test]
7559    fn test_parse_frontend_mutation_source_error_messages_are_user_facing() {
7560        for (source, expected_message) in [
7561            ("**", "Error parsing KCL source after editing: Unexpected token: *"),
7562            ("3'", "Error parsing KCL source after editing: found unknown token '''"),
7563        ] {
7564            let err = parse_frontend_mutation_source(
7565                source,
7566                "Error parsing KCL source after editing",
7567                "No AST produced after editing",
7568            )
7569            .expect_err("expected invalid KCL source to fail");
7570            let message = err.error.message();
7571
7572            assert_eq!(message, expected_message);
7573            assert!(!message.contains("CompilationIssue"));
7574            assert!(!message.contains("KclErrorDetails"));
7575            assert!(!message.contains("source_range"));
7576        }
7577    }
7578
7579    #[tokio::test(flavor = "multi_thread")]
7580    async fn test_edit_constraint_value_parse_error_messages_are_user_facing() {
7581        let initial_source = "\
7582sketch(on = XY) {
7583  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7584  distance([line1.start, line1.end]) == 10
7585}
7586";
7587        let program = Program::parse(initial_source).unwrap().0.unwrap();
7588
7589        let mut frontend = FrontendState::new();
7590        let mock_ctx = ExecutorContext::new_mock(None).await;
7591        let version = Version(0);
7592
7593        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7594        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7595        let sketch_id = sketch_object.id;
7596        let sketch = expect_sketch(sketch_object);
7597        let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7598
7599        for (value, expected_message) in [
7600            ("**", "Invalid constraint value: Unexpected token: *"),
7601            ("3'", "Invalid constraint value: found unknown token '''"),
7602        ] {
7603            let err = frontend
7604                .edit_constraint_value(&mock_ctx, version, sketch_id, constraint_id, value.to_owned())
7605                .await
7606                .expect_err("expected invalid constraint expression to fail");
7607            let message = err.error.message();
7608
7609            assert_eq!(message, expected_message);
7610            assert!(!message.contains("CompilationIssue"));
7611            assert!(!message.contains("KclErrorDetails"));
7612            assert!(!message.contains("source_range"));
7613        }
7614
7615        mock_ctx.close().await;
7616    }
7617
7618    #[tokio::test(flavor = "multi_thread")]
7619    async fn test_failed_edit_constraint_value_does_not_update_program() {
7620        let initial_source = "\
7621sketch(on = XY) {
7622  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7623  distance([line1.start, line1.end]) == 10
7624}
7625";
7626        let program = Program::parse(initial_source).unwrap().0.unwrap();
7627        let original_source = program.original_file_contents.clone();
7628
7629        let mut frontend = FrontendState::new();
7630        let mock_ctx = ExecutorContext::new_mock(None).await;
7631        let version = Version(0);
7632
7633        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7634        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7635        let sketch_id = sketch_object.id;
7636        let sketch = expect_sketch(sketch_object);
7637        let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7638
7639        frontend
7640            .edit_constraint_value(
7641                &mock_ctx,
7642                version,
7643                sketch_id,
7644                constraint_id,
7645                "unknownDistance".to_owned(),
7646            )
7647            .await
7648            .expect_err("expected invalid constraint value to fail execution");
7649
7650        assert_eq!(frontend.program.original_file_contents, original_source);
7651        assert!(!frontend.program.original_file_contents.contains("unknownDistance"));
7652
7653        mock_ctx.close().await;
7654    }
7655
7656    #[tokio::test(flavor = "multi_thread")]
7657    async fn test_edit_constraint_value_array_index_oob_fails_in_sketch_mode() {
7658        let initial_source = "\
7659arr = [0]
7660sketch(on = XY) {
7661  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7662  distance([line1.start, line1.end]) == 10
7663}
7664";
7665        let program = Program::parse(initial_source).unwrap().0.unwrap();
7666
7667        let mut frontend = FrontendState::new();
7668        let mock_ctx = ExecutorContext::new_mock(None).await;
7669        let version = Version(0);
7670
7671        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7672        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7673        let sketch_id = sketch_object.id;
7674        let sketch = expect_sketch(sketch_object);
7675        let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7676
7677        // In sketch mode execution, an out-of-bounds array index is an error.
7678        // It should not fall back to the first element of the array the way
7679        // plain mock execution does.
7680        let err = frontend
7681            .edit_constraint_value(&mock_ctx, version, sketch_id, constraint_id, "arr[5]".to_owned())
7682            .await
7683            .expect_err("expected out-of-bounds array index to be an error in sketch mode execution");
7684        let message = err.error.message();
7685        assert!(
7686            message.contains("The array doesn't have any item at index 5"),
7687            "unexpected error message: {message}"
7688        );
7689
7690        mock_ctx.close().await;
7691    }
7692
7693    #[tokio::test(flavor = "multi_thread")]
7694    async fn test_sketch_checkpoint_round_trip_restores_state() {
7695        let mut frontend = FrontendState::new();
7696        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7697        let mock_ctx = ExecutorContext::new_mock(None).await;
7698        let version = Version(0);
7699
7700        let (sketch_id, line_id, source_delta, scene_graph_delta) =
7701            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7702
7703        let expected_source = source_delta.text.clone();
7704        let expected_scene_graph = frontend.scene_graph.clone();
7705        let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
7706        let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
7707
7708        let checkpoint_id = frontend
7709            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7710            .await
7711            .unwrap();
7712
7713        let edited_segments = vec![ExistingSegmentCtor {
7714            id: line_id,
7715            ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
7716        }];
7717        let (edited_source, _edited_scene) = frontend
7718            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
7719            .await
7720            .unwrap();
7721        assert_ne!(edited_source.text, expected_source);
7722
7723        let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
7724
7725        assert_eq!(restored.source_delta.text, expected_source);
7726        assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
7727        assert!(restored.scene_graph_delta.invalidates_ids);
7728        assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
7729        assert_eq!(frontend.scene_graph, expected_scene_graph);
7730        assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
7731
7732        ctx.close().await;
7733    }
7734
7735    #[tokio::test(flavor = "multi_thread")]
7736    async fn test_sketch_checkpoints_prune_oldest_entries() {
7737        let mut frontend = FrontendState::new();
7738        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7739        let mock_ctx = ExecutorContext::new_mock(None).await;
7740        let version = Version(0);
7741
7742        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7743            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7744
7745        let mut checkpoint_ids = Vec::new();
7746        for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
7747            checkpoint_ids.push(
7748                frontend
7749                    .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7750                    .await
7751                    .unwrap(),
7752            );
7753        }
7754
7755        assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
7756        assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
7757
7758        let oldest_retained = checkpoint_ids[3];
7759        assert_eq!(
7760            frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
7761            Some(oldest_retained)
7762        );
7763
7764        let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
7765        assert!(evicted_restore.is_err());
7766        assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
7767
7768        frontend
7769            .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
7770            .await
7771            .unwrap();
7772
7773        ctx.close().await;
7774    }
7775
7776    #[tokio::test(flavor = "multi_thread")]
7777    async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
7778        let mut frontend = FrontendState::new();
7779        let missing_checkpoint = SketchCheckpointId::new(999);
7780
7781        let err = frontend
7782            .restore_sketch_checkpoint(missing_checkpoint)
7783            .await
7784            .expect_err("Expected restore to fail for missing checkpoint");
7785
7786        assert!(err.msg.contains("Sketch checkpoint not found"));
7787    }
7788
7789    #[tokio::test(flavor = "multi_thread")]
7790    async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
7791        let mut frontend = FrontendState::new();
7792        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7793        let mock_ctx = ExecutorContext::new_mock(None).await;
7794        let version = Version(0);
7795
7796        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7797            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7798
7799        let checkpoint_a = frontend
7800            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7801            .await
7802            .unwrap();
7803        let checkpoint_b = frontend
7804            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7805            .await
7806            .unwrap();
7807        assert_eq!(frontend.sketch_checkpoints.len(), 2);
7808
7809        frontend.clear_sketch_checkpoints();
7810        assert!(frontend.sketch_checkpoints.is_empty());
7811        frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
7812        frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
7813
7814        ctx.close().await;
7815    }
7816
7817    #[tokio::test(flavor = "multi_thread")]
7818    async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
7819        let mut frontend = FrontendState::new();
7820        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7821        let mock_ctx = ExecutorContext::new_mock(None).await;
7822        let version = Version(0);
7823
7824        let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
7825            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7826        let old_source = source_delta.text.clone();
7827        let old_checkpoint = frontend
7828            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7829            .await
7830            .unwrap();
7831        let initial_checkpoint_count = frontend.sketch_checkpoints.len();
7832
7833        let new_program = Program::parse("sketch(on = XY) {\n  point(at = [1mm, 2mm])\n}\n")
7834            .unwrap()
7835            .0
7836            .unwrap();
7837
7838        let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
7839        let SetProgramOutcome::Success {
7840            checkpoint_id: Some(new_checkpoint),
7841            ..
7842        } = result
7843        else {
7844            panic!("Expected Success with a fresh checkpoint baseline");
7845        };
7846
7847        assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
7848
7849        let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7850        assert_eq!(old_restore.source_delta.text, old_source);
7851
7852        let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
7853        assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
7854
7855        ctx.close().await;
7856    }
7857
7858    #[tokio::test(flavor = "multi_thread")]
7859    async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
7860        let mut frontend = FrontendState::new();
7861        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7862        let mock_ctx = ExecutorContext::new_mock(None).await;
7863        let version = Version(0);
7864
7865        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7866            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7867        let old_checkpoint = frontend
7868            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7869            .await
7870            .unwrap();
7871        let checkpoint_count_before = frontend.sketch_checkpoints.len();
7872
7873        let failing_program = Program::parse(
7874            "sketch(on = XY) {\n  line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
7875        )
7876        .unwrap()
7877        .0
7878        .unwrap();
7879
7880        let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
7881        assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
7882        assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
7883        frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7884
7885        ctx.close().await;
7886    }
7887
7888    #[tokio::test(flavor = "multi_thread")]
7889    async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
7890        let mut frontend = FrontendState::new();
7891        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7892
7893        let program = Program::parse(
7894            "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",
7895        )
7896        .unwrap()
7897        .0
7898        .unwrap();
7899        let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
7900        let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
7901            panic!("Expected successful baseline program execution");
7902        };
7903
7904        clear_mem_cache().await;
7905        assert!(read_old_memory().await.is_none());
7906
7907        let checkpoint_without_mock_memory = frontend
7908            .create_sketch_checkpoint((*exec_outcome).clone())
7909            .await
7910            .unwrap();
7911
7912        write_old_memory(SketchModeState::new_for_tests()).await;
7913        assert!(read_old_memory().await.is_some());
7914
7915        let checkpoint_with_mock_memory = frontend
7916            .create_sketch_checkpoint((*exec_outcome).clone())
7917            .await
7918            .unwrap();
7919
7920        clear_mem_cache().await;
7921        assert!(read_old_memory().await.is_none());
7922
7923        frontend
7924            .restore_sketch_checkpoint(checkpoint_with_mock_memory)
7925            .await
7926            .unwrap();
7927        assert!(read_old_memory().await.is_some());
7928
7929        frontend
7930            .restore_sketch_checkpoint(checkpoint_without_mock_memory)
7931            .await
7932            .unwrap();
7933        assert!(read_old_memory().await.is_none());
7934
7935        ctx.close().await;
7936    }
7937
7938    #[tokio::test(flavor = "multi_thread")]
7939    async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
7940        let source = "\
7941sketch(on = XY) {
7942  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
7943}
7944
7945bad = missing_name
7946";
7947        let program = Program::parse(source).unwrap().0.unwrap();
7948
7949        let mut frontend = FrontendState::new();
7950
7951        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7952        let mock_ctx = ExecutorContext::new_mock(None).await;
7953        let version = Version(0);
7954        let project_id = ProjectId(0);
7955        let file_id = FileId(0);
7956
7957        let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
7958            panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
7959        };
7960
7961        let sketch_id = frontend
7962            .scene_graph
7963            .objects
7964            .iter()
7965            .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
7966            .expect("Expected sketch object from errored hack_set_program");
7967
7968        frontend
7969            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
7970            .await
7971            .unwrap();
7972
7973        ctx.close().await;
7974        mock_ctx.close().await;
7975    }
7976
7977    #[tokio::test(flavor = "multi_thread")]
7978    async fn test_new_sketch_add_point_edit_point() {
7979        let program = Program::empty();
7980
7981        let mut frontend = FrontendState::new();
7982        frontend.program = program;
7983
7984        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7985        let mock_ctx = ExecutorContext::new_mock(None).await;
7986        let version = Version(0);
7987
7988        let sketch_args = SketchCtor {
7989            on: Plane::Default(PlaneName::Xy),
7990        };
7991        let (_src_delta, scene_delta, sketch_id) = frontend
7992            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7993            .await
7994            .unwrap();
7995        assert_eq!(sketch_id, ObjectId(1));
7996        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7997        let sketch_object = &scene_delta.new_graph.objects[1];
7998        assert_eq!(sketch_object.id, ObjectId(1));
7999        assert_eq!(
8000            sketch_object.kind,
8001            ObjectKind::Sketch(Sketch {
8002                args: SketchCtor {
8003                    on: Plane::Default(PlaneName::Xy)
8004                },
8005                plane: ObjectId(0),
8006                segments: vec![],
8007                constraints: vec![],
8008            })
8009        );
8010        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8011
8012        let point_ctor = PointCtor {
8013            position: Point2d {
8014                x: Expr::Number(Number {
8015                    value: 1.0,
8016                    units: NumericSuffix::Inch,
8017                }),
8018                y: Expr::Number(Number {
8019                    value: 2.0,
8020                    units: NumericSuffix::Inch,
8021                }),
8022            },
8023        };
8024        let segment = SegmentCtor::Point(point_ctor);
8025        let (src_delta, scene_delta) = frontend
8026            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8027            .await
8028            .unwrap();
8029        insta::assert_snapshot!("test_new_sketch_add_point_edit_point_1", src_delta.text.as_str());
8030        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
8031        assert_eq!(scene_delta.new_graph.objects.len(), 3);
8032        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8033            assert_eq!(scene_object.id.0, i);
8034        }
8035
8036        let point_id = *scene_delta.new_objects.last().unwrap();
8037
8038        let point_ctor = PointCtor {
8039            position: Point2d {
8040                x: Expr::Number(Number {
8041                    value: 3.0,
8042                    units: NumericSuffix::Inch,
8043                }),
8044                y: Expr::Number(Number {
8045                    value: 4.0,
8046                    units: NumericSuffix::Inch,
8047                }),
8048            },
8049        };
8050        let segments = vec![ExistingSegmentCtor {
8051            id: point_id,
8052            ctor: SegmentCtor::Point(point_ctor),
8053        }];
8054        let (src_delta, scene_delta) = frontend
8055            .edit_segments(&mock_ctx, version, sketch_id, segments)
8056            .await
8057            .unwrap();
8058        insta::assert_snapshot!("test_new_sketch_add_point_edit_point_2", src_delta.text.as_str());
8059        assert_eq!(scene_delta.new_objects, vec![]);
8060        assert_eq!(scene_delta.new_graph.objects.len(), 3);
8061
8062        ctx.close().await;
8063        mock_ctx.close().await;
8064    }
8065
8066    #[tokio::test(flavor = "multi_thread")]
8067    async fn test_new_sketch_add_line_edit_line() {
8068        let program = Program::empty();
8069
8070        let mut frontend = FrontendState::new();
8071        frontend.program = program;
8072
8073        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8074        let mock_ctx = ExecutorContext::new_mock(None).await;
8075        let version = Version(0);
8076
8077        let sketch_args = SketchCtor {
8078            on: Plane::Default(PlaneName::Xy),
8079        };
8080        let (_src_delta, scene_delta, sketch_id) = frontend
8081            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8082            .await
8083            .unwrap();
8084        assert_eq!(sketch_id, ObjectId(1));
8085        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8086        let sketch_object = &scene_delta.new_graph.objects[1];
8087        assert_eq!(sketch_object.id, ObjectId(1));
8088        assert_eq!(
8089            sketch_object.kind,
8090            ObjectKind::Sketch(Sketch {
8091                args: SketchCtor {
8092                    on: Plane::Default(PlaneName::Xy)
8093                },
8094                plane: ObjectId(0),
8095                segments: vec![],
8096                constraints: vec![],
8097            })
8098        );
8099        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8100
8101        let line_ctor = LineCtor {
8102            start: Point2d {
8103                x: Expr::Number(Number {
8104                    value: 0.0,
8105                    units: NumericSuffix::Mm,
8106                }),
8107                y: Expr::Number(Number {
8108                    value: 0.0,
8109                    units: NumericSuffix::Mm,
8110                }),
8111            },
8112            end: Point2d {
8113                x: Expr::Number(Number {
8114                    value: 10.0,
8115                    units: NumericSuffix::Mm,
8116                }),
8117                y: Expr::Number(Number {
8118                    value: 10.0,
8119                    units: NumericSuffix::Mm,
8120                }),
8121            },
8122            construction: None,
8123        };
8124        let segment = SegmentCtor::Line(line_ctor);
8125        let (src_delta, scene_delta) = frontend
8126            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8127            .await
8128            .unwrap();
8129        insta::assert_snapshot!("test_new_sketch_add_line_edit_line_1", src_delta.text.as_str());
8130        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8131        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8132        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8133            assert_eq!(scene_object.id.0, i);
8134        }
8135
8136        // The new objects are the end points and then the line.
8137        let line = *scene_delta.new_objects.last().unwrap();
8138
8139        let line_ctor = LineCtor {
8140            start: Point2d {
8141                x: Expr::Number(Number {
8142                    value: 1.0,
8143                    units: NumericSuffix::Mm,
8144                }),
8145                y: Expr::Number(Number {
8146                    value: 2.0,
8147                    units: NumericSuffix::Mm,
8148                }),
8149            },
8150            end: Point2d {
8151                x: Expr::Number(Number {
8152                    value: 13.0,
8153                    units: NumericSuffix::Mm,
8154                }),
8155                y: Expr::Number(Number {
8156                    value: 14.0,
8157                    units: NumericSuffix::Mm,
8158                }),
8159            },
8160            construction: None,
8161        };
8162        let segments = vec![ExistingSegmentCtor {
8163            id: line,
8164            ctor: SegmentCtor::Line(line_ctor),
8165        }];
8166        let (src_delta, scene_delta) = frontend
8167            .edit_segments(&mock_ctx, version, sketch_id, segments)
8168            .await
8169            .unwrap();
8170        insta::assert_snapshot!("test_new_sketch_add_line_edit_line_2", src_delta.text.as_str());
8171        assert_eq!(scene_delta.new_objects, vec![]);
8172        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8173
8174        ctx.close().await;
8175        mock_ctx.close().await;
8176    }
8177
8178    #[tokio::test(flavor = "multi_thread")]
8179    async fn test_new_sketch_add_arc_edit_arc() {
8180        let program = Program::empty();
8181
8182        let mut frontend = FrontendState::new();
8183        frontend.program = program;
8184
8185        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8186        let mock_ctx = ExecutorContext::new_mock(None).await;
8187        let version = Version(0);
8188
8189        let sketch_args = SketchCtor {
8190            on: Plane::Default(PlaneName::Xy),
8191        };
8192        let (_src_delta, scene_delta, sketch_id) = frontend
8193            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8194            .await
8195            .unwrap();
8196        assert_eq!(sketch_id, ObjectId(1));
8197        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8198        let sketch_object = &scene_delta.new_graph.objects[1];
8199        assert_eq!(sketch_object.id, ObjectId(1));
8200        assert_eq!(
8201            sketch_object.kind,
8202            ObjectKind::Sketch(Sketch {
8203                args: SketchCtor {
8204                    on: Plane::Default(PlaneName::Xy),
8205                },
8206                plane: ObjectId(0),
8207                segments: vec![],
8208                constraints: vec![],
8209            })
8210        );
8211        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8212
8213        let arc_ctor = ArcCtor {
8214            start: Point2d {
8215                x: Expr::Var(Number {
8216                    value: 0.0,
8217                    units: NumericSuffix::Mm,
8218                }),
8219                y: Expr::Var(Number {
8220                    value: 0.0,
8221                    units: NumericSuffix::Mm,
8222                }),
8223            },
8224            end: Point2d {
8225                x: Expr::Var(Number {
8226                    value: 10.0,
8227                    units: NumericSuffix::Mm,
8228                }),
8229                y: Expr::Var(Number {
8230                    value: 10.0,
8231                    units: NumericSuffix::Mm,
8232                }),
8233            },
8234            center: Point2d {
8235                x: Expr::Var(Number {
8236                    value: 10.0,
8237                    units: NumericSuffix::Mm,
8238                }),
8239                y: Expr::Var(Number {
8240                    value: 0.0,
8241                    units: NumericSuffix::Mm,
8242                }),
8243            },
8244            direction: None,
8245            construction: None,
8246        };
8247        let segment = SegmentCtor::Arc(arc_ctor);
8248        let (src_delta, scene_delta) = frontend
8249            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8250            .await
8251            .unwrap();
8252        insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_1", src_delta.text.as_str());
8253        assert_eq!(
8254            scene_delta.new_objects,
8255            vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
8256        );
8257        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8258            assert_eq!(scene_object.id.0, i);
8259        }
8260        assert_eq!(scene_delta.new_graph.objects.len(), 6);
8261
8262        // The new objects are the end points, the center, and then the arc.
8263        let arc = *scene_delta.new_objects.last().unwrap();
8264
8265        let arc_ctor = ArcCtor {
8266            start: Point2d {
8267                x: Expr::Var(Number {
8268                    value: 1.0,
8269                    units: NumericSuffix::Mm,
8270                }),
8271                y: Expr::Var(Number {
8272                    value: 2.0,
8273                    units: NumericSuffix::Mm,
8274                }),
8275            },
8276            end: Point2d {
8277                x: Expr::Var(Number {
8278                    value: 13.0,
8279                    units: NumericSuffix::Mm,
8280                }),
8281                y: Expr::Var(Number {
8282                    value: 14.0,
8283                    units: NumericSuffix::Mm,
8284                }),
8285            },
8286            center: Point2d {
8287                x: Expr::Var(Number {
8288                    value: 13.0,
8289                    units: NumericSuffix::Mm,
8290                }),
8291                y: Expr::Var(Number {
8292                    value: 2.0,
8293                    units: NumericSuffix::Mm,
8294                }),
8295            },
8296            direction: None,
8297            construction: None,
8298        };
8299        let segments = vec![ExistingSegmentCtor {
8300            id: arc,
8301            ctor: SegmentCtor::Arc(arc_ctor),
8302        }];
8303        let (src_delta, scene_delta) = frontend
8304            .edit_segments(&mock_ctx, version, sketch_id, segments)
8305            .await
8306            .unwrap();
8307        insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_2", src_delta.text.as_str());
8308        assert_eq!(scene_delta.new_objects, vec![]);
8309        assert_eq!(scene_delta.new_graph.objects.len(), 6);
8310
8311        ctx.close().await;
8312        mock_ctx.close().await;
8313    }
8314
8315    #[tokio::test(flavor = "multi_thread")]
8316    async fn test_new_sketch_add_circle_edit_circle() {
8317        let program = Program::empty();
8318
8319        let mut frontend = FrontendState::new();
8320        frontend.program = program;
8321
8322        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8323        let mock_ctx = ExecutorContext::new_mock(None).await;
8324        let version = Version(0);
8325
8326        let sketch_args = SketchCtor {
8327            on: Plane::Default(PlaneName::Xy),
8328        };
8329        let (_src_delta, _scene_delta, sketch_id) = frontend
8330            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8331            .await
8332            .unwrap();
8333
8334        // Add a circle segment.
8335        let circle_ctor = CircleCtor {
8336            start: Point2d {
8337                x: Expr::Var(Number {
8338                    value: 5.0,
8339                    units: NumericSuffix::Mm,
8340                }),
8341                y: Expr::Var(Number {
8342                    value: 0.0,
8343                    units: NumericSuffix::Mm,
8344                }),
8345            },
8346            center: Point2d {
8347                x: Expr::Var(Number {
8348                    value: 0.0,
8349                    units: NumericSuffix::Mm,
8350                }),
8351                y: Expr::Var(Number {
8352                    value: 0.0,
8353                    units: NumericSuffix::Mm,
8354                }),
8355            },
8356            construction: None,
8357        };
8358        let segment = SegmentCtor::Circle(circle_ctor);
8359        let (src_delta, scene_delta) = frontend
8360            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8361            .await
8362            .unwrap();
8363        insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_1", src_delta.text.as_str());
8364        // The new objects are start, center, and then the circle segment.
8365        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8366        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8367
8368        let circle = *scene_delta.new_objects.last().unwrap();
8369
8370        // Edit the circle segment.
8371        let circle_ctor = CircleCtor {
8372            start: Point2d {
8373                x: Expr::Var(Number {
8374                    value: 10.0,
8375                    units: NumericSuffix::Mm,
8376                }),
8377                y: Expr::Var(Number {
8378                    value: 0.0,
8379                    units: NumericSuffix::Mm,
8380                }),
8381            },
8382            center: Point2d {
8383                x: Expr::Var(Number {
8384                    value: 3.0,
8385                    units: NumericSuffix::Mm,
8386                }),
8387                y: Expr::Var(Number {
8388                    value: 4.0,
8389                    units: NumericSuffix::Mm,
8390                }),
8391            },
8392            construction: None,
8393        };
8394        let segments = vec![ExistingSegmentCtor {
8395            id: circle,
8396            ctor: SegmentCtor::Circle(circle_ctor),
8397        }];
8398        let (src_delta, scene_delta) = frontend
8399            .edit_segments(&mock_ctx, version, sketch_id, segments)
8400            .await
8401            .unwrap();
8402        insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_2", src_delta.text.as_str());
8403        assert_eq!(scene_delta.new_objects, vec![]);
8404        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8405
8406        ctx.close().await;
8407        mock_ctx.close().await;
8408    }
8409
8410    #[tokio::test(flavor = "multi_thread")]
8411    async fn test_delete_circle() {
8412        let initial_source = "sketch001 = sketch(on = XY) {
8413  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8414}
8415";
8416
8417        let program = Program::parse(initial_source).unwrap().0.unwrap();
8418        let mut frontend = FrontendState::new();
8419
8420        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8421        let mock_ctx = ExecutorContext::new_mock(None).await;
8422        let version = Version(0);
8423
8424        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8425        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8426        let sketch_id = sketch_object.id;
8427        let sketch = expect_sketch(sketch_object);
8428
8429        // The sketch should have 3 segments: start point, center point, and the circle.
8430        assert_eq!(sketch.segments.len(), 3);
8431        let circle_id = sketch.segments[2];
8432
8433        // Delete the circle.
8434        let (src_delta, scene_delta) = frontend
8435            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
8436            .await
8437            .unwrap();
8438        insta::assert_snapshot!("test_delete_circle", src_delta.text.as_str());
8439        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8440        let new_sketch = expect_sketch(new_sketch_object);
8441        assert_eq!(new_sketch.segments.len(), 0);
8442
8443        ctx.close().await;
8444        mock_ctx.close().await;
8445    }
8446
8447    #[tokio::test(flavor = "multi_thread")]
8448    async fn test_edit_circle_via_point() {
8449        let initial_source = "sketch001 = sketch(on = XY) {
8450  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8451}
8452";
8453
8454        let program = Program::parse(initial_source).unwrap().0.unwrap();
8455        let mut frontend = FrontendState::new();
8456
8457        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8458        let mock_ctx = ExecutorContext::new_mock(None).await;
8459        let version = Version(0);
8460
8461        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8462        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8463        let sketch_id = sketch_object.id;
8464        let sketch = expect_sketch(sketch_object);
8465
8466        // Find the circle segment and its start point.
8467        let circle_id = sketch
8468            .segments
8469            .iter()
8470            .copied()
8471            .find(|seg_id| {
8472                matches!(
8473                    &frontend.scene_graph.objects[seg_id.0].kind,
8474                    ObjectKind::Segment {
8475                        segment: Segment::Circle(_)
8476                    }
8477                )
8478            })
8479            .expect("Expected a circle segment in sketch");
8480        let circle_object = &frontend.scene_graph.objects[circle_id.0];
8481        let ObjectKind::Segment {
8482            segment: Segment::Circle(circle),
8483        } = &circle_object.kind
8484        else {
8485            panic!("Expected circle segment, got: {:?}", circle_object.kind);
8486        };
8487        let start_point_id = circle.start;
8488
8489        // Edit the start point via SegmentCtor::Point.
8490        let segments = vec![ExistingSegmentCtor {
8491            id: start_point_id,
8492            ctor: SegmentCtor::Point(PointCtor {
8493                position: Point2d {
8494                    x: Expr::Var(Number {
8495                        value: 7.0,
8496                        units: NumericSuffix::Mm,
8497                    }),
8498                    y: Expr::Var(Number {
8499                        value: 1.0,
8500                        units: NumericSuffix::Mm,
8501                    }),
8502                },
8503            }),
8504        }];
8505        let (src_delta, _scene_delta) = frontend
8506            .edit_segments(&mock_ctx, version, sketch_id, segments)
8507            .await
8508            .unwrap();
8509        insta::assert_snapshot!("test_edit_circle_via_point", src_delta.text.as_str());
8510
8511        ctx.close().await;
8512        mock_ctx.close().await;
8513    }
8514
8515    #[tokio::test(flavor = "multi_thread")]
8516    async fn test_add_line_when_sketch_block_uses_variable() {
8517        let initial_source = "s = sketch(on = XY) {}
8518";
8519
8520        let program = Program::parse(initial_source).unwrap().0.unwrap();
8521
8522        let mut frontend = FrontendState::new();
8523
8524        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8525        let mock_ctx = ExecutorContext::new_mock(None).await;
8526        let version = Version(0);
8527
8528        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8529        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8530        let sketch_id = sketch_object.id;
8531
8532        let line_ctor = LineCtor {
8533            start: Point2d {
8534                x: Expr::Number(Number {
8535                    value: 0.0,
8536                    units: NumericSuffix::Mm,
8537                }),
8538                y: Expr::Number(Number {
8539                    value: 0.0,
8540                    units: NumericSuffix::Mm,
8541                }),
8542            },
8543            end: Point2d {
8544                x: Expr::Number(Number {
8545                    value: 10.0,
8546                    units: NumericSuffix::Mm,
8547                }),
8548                y: Expr::Number(Number {
8549                    value: 10.0,
8550                    units: NumericSuffix::Mm,
8551                }),
8552            },
8553            construction: None,
8554        };
8555        let segment = SegmentCtor::Line(line_ctor);
8556        let (src_delta, scene_delta) = frontend
8557            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8558            .await
8559            .unwrap();
8560        insta::assert_snapshot!("test_add_line_when_sketch_block_uses_variable", src_delta.text.as_str());
8561        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8562        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8563
8564        ctx.close().await;
8565        mock_ctx.close().await;
8566    }
8567
8568    #[tokio::test(flavor = "multi_thread")]
8569    async fn test_new_sketch_add_line_delete_sketch() {
8570        let program = Program::empty();
8571
8572        let mut frontend = FrontendState::new();
8573        frontend.program = program;
8574
8575        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8576        let mock_ctx = ExecutorContext::new_mock(None).await;
8577        let version = Version(0);
8578
8579        let sketch_args = SketchCtor {
8580            on: Plane::Default(PlaneName::Xy),
8581        };
8582        let (_src_delta, scene_delta, sketch_id) = frontend
8583            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8584            .await
8585            .unwrap();
8586        assert_eq!(sketch_id, ObjectId(1));
8587        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8588        let sketch_object = &scene_delta.new_graph.objects[1];
8589        assert_eq!(sketch_object.id, ObjectId(1));
8590        assert_eq!(
8591            sketch_object.kind,
8592            ObjectKind::Sketch(Sketch {
8593                args: SketchCtor {
8594                    on: Plane::Default(PlaneName::Xy)
8595                },
8596                plane: ObjectId(0),
8597                segments: vec![],
8598                constraints: vec![],
8599            })
8600        );
8601        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8602
8603        let line_ctor = LineCtor {
8604            start: Point2d {
8605                x: Expr::Number(Number {
8606                    value: 0.0,
8607                    units: NumericSuffix::Mm,
8608                }),
8609                y: Expr::Number(Number {
8610                    value: 0.0,
8611                    units: NumericSuffix::Mm,
8612                }),
8613            },
8614            end: Point2d {
8615                x: Expr::Number(Number {
8616                    value: 10.0,
8617                    units: NumericSuffix::Mm,
8618                }),
8619                y: Expr::Number(Number {
8620                    value: 10.0,
8621                    units: NumericSuffix::Mm,
8622                }),
8623            },
8624            construction: None,
8625        };
8626        let segment = SegmentCtor::Line(line_ctor);
8627        let (src_delta, scene_delta) = frontend
8628            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8629            .await
8630            .unwrap();
8631        insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_1", src_delta.text.as_str());
8632        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8633
8634        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8635        insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_2", src_delta.text.as_str());
8636        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8637
8638        ctx.close().await;
8639        mock_ctx.close().await;
8640    }
8641
8642    #[tokio::test(flavor = "multi_thread")]
8643    async fn test_delete_sketch_when_sketch_block_uses_variable() {
8644        let initial_source = "s = sketch(on = XY) {}
8645";
8646
8647        let program = Program::parse(initial_source).unwrap().0.unwrap();
8648
8649        let mut frontend = FrontendState::new();
8650
8651        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8652        let version = Version(0);
8653
8654        frontend.hack_set_program(&ctx, program).await.unwrap();
8655        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8656        let sketch_id = sketch_object.id;
8657
8658        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8659        insta::assert_snapshot!(
8660            "test_delete_sketch_when_sketch_block_uses_variable",
8661            src_delta.text.as_str()
8662        );
8663        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8664
8665        ctx.close().await;
8666    }
8667
8668    #[tokio::test(flavor = "multi_thread")]
8669    async fn test_delete_sketch_after_comment() {
8670        let initial_source = "sketch001 = sketch(on = XZ) {
8671}
8672";
8673
8674        let program = Program::parse(initial_source).unwrap().0.unwrap();
8675        let mut frontend = FrontendState::new();
8676
8677        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8678        let version = Version(0);
8679
8680        frontend.hack_set_program(&ctx, program).await.unwrap();
8681        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8682        let sketch_id = sketch_object.id;
8683        let original_source = sketch_object.source.clone();
8684
8685        let commented_source = "// test 1
8686sketch001 = sketch(on = XZ) {
8687}
8688";
8689        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8690        frontend.engine_execute(&ctx, commented_program).await.unwrap();
8691
8692        let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
8693        assert_eq!(cached_sketch_object.source, original_source);
8694
8695        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8696        assert!(
8697            !src_delta.text.contains("sketch001"),
8698            "sketch was not deleted: {}",
8699            src_delta.text
8700        );
8701        // The leading line comment must survive deletion.
8702        insta::assert_snapshot!("test_delete_sketch_after_comment", src_delta.text.as_str());
8703        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8704
8705        ctx.close().await;
8706    }
8707
8708    #[tokio::test(flavor = "multi_thread")]
8709    async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
8710        let initial_source = "sketch001 = sketch(on = XZ) {
8711}
8712foo = 1
8713";
8714
8715        let program = Program::parse(initial_source).unwrap().0.unwrap();
8716        let mut frontend = FrontendState::new();
8717
8718        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8719        let version = Version(0);
8720
8721        frontend.hack_set_program(&ctx, program).await.unwrap();
8722        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8723        let sketch_id = sketch_object.id;
8724
8725        let commented_source = "// keep me
8726sketch001 = sketch(on = XZ) {
8727}
8728foo = 1
8729";
8730        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8731        frontend.engine_execute(&ctx, commented_program).await.unwrap();
8732
8733        let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8734        // The leading comment should remain, now attached to the following body item.
8735        insta::assert_snapshot!(
8736            "test_delete_sketch_preserves_pre_comment_when_followed_by_code",
8737            src_delta.text.as_str()
8738        );
8739
8740        ctx.close().await;
8741    }
8742
8743    #[tokio::test(flavor = "multi_thread")]
8744    async fn test_delete_segment_preserves_pre_comment() {
8745        let initial_source = "\
8746sketch(on = XY) {
8747  point(at = [var 1, var 2])
8748  // describe the middle point
8749  point(at = [var 3, var 4])
8750  point(at = [var 5, var 6])
8751}
8752";
8753
8754        let program = Program::parse(initial_source).unwrap().0.unwrap();
8755        let mut frontend = FrontendState::new();
8756
8757        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8758        let mock_ctx = ExecutorContext::new_mock(None).await;
8759        let version = Version(0);
8760
8761        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8762        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8763        let sketch_id = sketch_object.id;
8764        let sketch = expect_sketch(sketch_object);
8765
8766        let middle_point_id = *sketch.segments.get(1).unwrap();
8767
8768        let (src_delta, _scene_delta) = frontend
8769            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8770            .await
8771            .unwrap();
8772        // The line comment on the line above the deleted point must be preserved.
8773        // It is reattached to the next surviving body item.
8774        insta::assert_snapshot!("test_delete_segment_preserves_pre_comment", src_delta.text.as_str());
8775
8776        ctx.close().await;
8777        mock_ctx.close().await;
8778    }
8779
8780    #[tokio::test(flavor = "multi_thread")]
8781    async fn test_delete_last_segment_preserves_pre_comment() {
8782        let initial_source = "\
8783sketch(on = XY) {
8784  point(at = [var 1, var 2])
8785  // describe the trailing point
8786  point(at = [var 3, var 4])
8787}
8788";
8789
8790        let program = Program::parse(initial_source).unwrap().0.unwrap();
8791        let mut frontend = FrontendState::new();
8792
8793        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8794        let mock_ctx = ExecutorContext::new_mock(None).await;
8795        let version = Version(0);
8796
8797        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8798        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8799        let sketch_id = sketch_object.id;
8800        let sketch = expect_sketch(sketch_object);
8801
8802        let last_point_id = *sketch.segments.last().unwrap();
8803
8804        let (src_delta, _scene_delta) = frontend
8805            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
8806            .await
8807            .unwrap();
8808        // No following item to attach to; the comment is kept inside the sketch
8809        // block as trailing non-code metadata so the user does not lose it.
8810        insta::assert_snapshot!(
8811            "test_delete_last_segment_preserves_pre_comment",
8812            src_delta.text.as_str()
8813        );
8814
8815        ctx.close().await;
8816        mock_ctx.close().await;
8817    }
8818
8819    #[tokio::test(flavor = "multi_thread")]
8820    async fn test_delete_segment_drops_inline_trailing_comment() {
8821        let initial_source = "\
8822sketch(on = XY) {
8823  point(at = [var 1, var 2])
8824  point(at = [var 3, var 4]) // same-line note that gets dropped
8825  point(at = [var 5, var 6])
8826}
8827";
8828
8829        let program = Program::parse(initial_source).unwrap().0.unwrap();
8830        let mut frontend = FrontendState::new();
8831
8832        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8833        let mock_ctx = ExecutorContext::new_mock(None).await;
8834        let version = Version(0);
8835
8836        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8837        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8838        let sketch_id = sketch_object.id;
8839        let sketch = expect_sketch(sketch_object);
8840
8841        let middle_point_id = *sketch.segments.get(1).unwrap();
8842
8843        let (src_delta, _scene_delta) = frontend
8844            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8845            .await
8846            .unwrap();
8847        // The same-line trailing comment is removed along with the deleted code.
8848        assert!(
8849            !src_delta.text.contains("same-line note"),
8850            "inline comment should have been removed: {}",
8851            src_delta.text
8852        );
8853
8854        ctx.close().await;
8855        mock_ctx.close().await;
8856    }
8857
8858    #[tokio::test(flavor = "multi_thread")]
8859    async fn test_delete_segments_preserves_block_comments_across_positions() {
8860        // One test exercising several `delete_body_item_preserving_pre_comments`
8861        // branches at once with `/* ... */` block comments:
8862        //   - first point: leading block comment must migrate to the next item.
8863        //   - first point: same-line trailing block comment must be dropped.
8864        //   - middle point: leading block comment must stay attached after migration.
8865        //   - last point: leading block comment, with no surviving next item,
8866        //     must be converted into a trailing NonCodeNode.
8867        let initial_source = "\
8868sketch(on = XY) {
8869  /* above first - moves to middle */
8870  point(at = [var 1, var 2]) /* same-line on first - dropped */
8871  /* above middle - stays */
8872  point(at = [var 3, var 4])
8873  /* above last - moves to trailing meta */
8874  point(at = [var 5, var 6])
8875}
8876";
8877
8878        let program = Program::parse(initial_source).unwrap().0.unwrap();
8879        let mut frontend = FrontendState::new();
8880
8881        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8882        let mock_ctx = ExecutorContext::new_mock(None).await;
8883        let version = Version(0);
8884
8885        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8886        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8887        let sketch_id = sketch_object.id;
8888        let sketch = expect_sketch(sketch_object);
8889
8890        let first_point_id = *sketch.segments.first().unwrap();
8891        let last_point_id = *sketch.segments.last().unwrap();
8892
8893        let (src_delta, _scene_delta) = frontend
8894            .delete_objects(
8895                &mock_ctx,
8896                version,
8897                sketch_id,
8898                Vec::new(),
8899                vec![first_point_id, last_point_id],
8900            )
8901            .await
8902            .unwrap();
8903        insta::assert_snapshot!(
8904            "test_delete_segments_preserves_block_comments_across_positions",
8905            src_delta.text.as_str()
8906        );
8907
8908        ctx.close().await;
8909        mock_ctx.close().await;
8910    }
8911
8912    #[tokio::test(flavor = "multi_thread")]
8913    async fn test_edit_line_when_editing_its_start_point() {
8914        let initial_source = "\
8915sketch(on = XY) {
8916  line(start = [var 1, var 2], end = [var 3, var 4])
8917}
8918";
8919
8920        let program = Program::parse(initial_source).unwrap().0.unwrap();
8921
8922        let mut frontend = FrontendState::new();
8923
8924        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8925        let mock_ctx = ExecutorContext::new_mock(None).await;
8926        let version = Version(0);
8927
8928        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8929        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8930        let sketch_id = sketch_object.id;
8931        let sketch = expect_sketch(sketch_object);
8932
8933        let point_id = *sketch.segments.first().unwrap();
8934
8935        let point_ctor = PointCtor {
8936            position: Point2d {
8937                x: Expr::Var(Number {
8938                    value: 5.0,
8939                    units: NumericSuffix::Inch,
8940                }),
8941                y: Expr::Var(Number {
8942                    value: 6.0,
8943                    units: NumericSuffix::Inch,
8944                }),
8945            },
8946        };
8947        let segments = vec![ExistingSegmentCtor {
8948            id: point_id,
8949            ctor: SegmentCtor::Point(point_ctor),
8950        }];
8951        let (src_delta, scene_delta) = frontend
8952            .edit_segments(&mock_ctx, version, sketch_id, segments)
8953            .await
8954            .unwrap();
8955        insta::assert_snapshot!("test_edit_line_when_editing_its_start_point", src_delta.text.as_str());
8956        assert_eq!(scene_delta.new_objects, vec![]);
8957        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8958
8959        ctx.close().await;
8960        mock_ctx.close().await;
8961    }
8962
8963    #[tokio::test(flavor = "multi_thread")]
8964    async fn test_edit_line_when_editing_its_end_point() {
8965        let initial_source = "\
8966sketch(on = XY) {
8967  line(start = [var 1, var 2], end = [var 3, var 4])
8968}
8969";
8970
8971        let program = Program::parse(initial_source).unwrap().0.unwrap();
8972
8973        let mut frontend = FrontendState::new();
8974
8975        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8976        let mock_ctx = ExecutorContext::new_mock(None).await;
8977        let version = Version(0);
8978
8979        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8980        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8981        let sketch_id = sketch_object.id;
8982        let sketch = expect_sketch(sketch_object);
8983        let point_id = *sketch.segments.get(1).unwrap();
8984
8985        let point_ctor = PointCtor {
8986            position: Point2d {
8987                x: Expr::Var(Number {
8988                    value: 5.0,
8989                    units: NumericSuffix::Inch,
8990                }),
8991                y: Expr::Var(Number {
8992                    value: 6.0,
8993                    units: NumericSuffix::Inch,
8994                }),
8995            },
8996        };
8997        let segments = vec![ExistingSegmentCtor {
8998            id: point_id,
8999            ctor: SegmentCtor::Point(point_ctor),
9000        }];
9001        let (src_delta, scene_delta) = frontend
9002            .edit_segments(&mock_ctx, version, sketch_id, segments)
9003            .await
9004            .unwrap();
9005        insta::assert_snapshot!("test_edit_line_when_editing_its_end_point", src_delta.text.as_str());
9006        assert_eq!(scene_delta.new_objects, vec![]);
9007        assert_eq!(
9008            scene_delta.new_graph.objects.len(),
9009            5,
9010            "{:#?}",
9011            scene_delta.new_graph.objects
9012        );
9013
9014        ctx.close().await;
9015        mock_ctx.close().await;
9016    }
9017
9018    #[tokio::test(flavor = "multi_thread")]
9019    async fn test_edit_line_with_coincident_feedback() {
9020        let initial_source = "\
9021sketch(on = XY) {
9022  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9023  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9024  fixed([line1.start, [0, 0]])
9025  coincident([line1.end, line2.start])
9026  equalLength([line1, line2])
9027}
9028";
9029
9030        let program = Program::parse(initial_source).unwrap().0.unwrap();
9031
9032        let mut frontend = FrontendState::new();
9033
9034        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9035        let mock_ctx = ExecutorContext::new_mock(None).await;
9036        let version = Version(0);
9037
9038        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9039        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9040        let sketch_id = sketch_object.id;
9041        let sketch = expect_sketch(sketch_object);
9042        let line2_end_id = *sketch.segments.get(4).unwrap();
9043
9044        let segments = vec![ExistingSegmentCtor {
9045            id: line2_end_id,
9046            ctor: SegmentCtor::Point(PointCtor {
9047                position: Point2d {
9048                    x: Expr::Var(Number {
9049                        value: 9.0,
9050                        units: NumericSuffix::None,
9051                    }),
9052                    y: Expr::Var(Number {
9053                        value: 10.0,
9054                        units: NumericSuffix::None,
9055                    }),
9056                },
9057            }),
9058        }];
9059        let (src_delta, scene_delta) = frontend
9060            .edit_segments(&mock_ctx, version, sketch_id, segments)
9061            .await
9062            .unwrap();
9063        insta::assert_snapshot!("test_edit_line_with_coincident_feedback", src_delta.text.as_str());
9064        assert_eq!(
9065            scene_delta.new_graph.objects.len(),
9066            11,
9067            "{:#?}",
9068            scene_delta.new_graph.objects
9069        );
9070
9071        ctx.close().await;
9072        mock_ctx.close().await;
9073    }
9074
9075    #[tokio::test(flavor = "multi_thread")]
9076    async fn test_edit_segments_persists_solver_feedback_for_next_mock_execute() {
9077        let initial_source = "\
9078sketch(on = XY) {
9079  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9080  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9081  fixed([line1.start, [0, 0]])
9082  coincident([line1.end, line2.start])
9083  equalLength([line1, line2])
9084}
9085";
9086
9087        let program = Program::parse(initial_source).unwrap().0.unwrap();
9088        let mut frontend = FrontendState::new();
9089        let mock_ctx = ExecutorContext::new_mock(None).await;
9090        let version = Version(0);
9091
9092        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9093        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9094        let sketch_id = sketch_object.id;
9095        let sketch = expect_sketch(sketch_object);
9096        let line2_end_id = *sketch.segments.get(4).unwrap();
9097
9098        let segments = vec![ExistingSegmentCtor {
9099            id: line2_end_id,
9100            ctor: SegmentCtor::Point(PointCtor {
9101                position: Point2d {
9102                    x: Expr::Var(Number {
9103                        value: 9.0,
9104                        units: NumericSuffix::None,
9105                    }),
9106                    y: Expr::Var(Number {
9107                        value: 10.0,
9108                        units: NumericSuffix::None,
9109                    }),
9110                },
9111            }),
9112        }];
9113        let (edited_source, _) = frontend
9114            .edit_segments(&mock_ctx, version, sketch_id, segments)
9115            .await
9116            .unwrap();
9117
9118        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9119        assert_eq!(mock_source.text, edited_source.text);
9120
9121        mock_ctx.close().await;
9122    }
9123
9124    /// Preview segment edits should return solved geometry without persisting
9125    /// solver feedback to KCL.
9126    #[tokio::test(flavor = "multi_thread")]
9127    async fn test_preview_edit_segments_does_not_persist_solver_feedback() {
9128        let initial_source = "\
9129sketch(on = XY) {
9130  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9131  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9132  fixed([line1.start, [0, 0]])
9133  coincident([line1.end, line2.start])
9134  equalLength([line1, line2])
9135}
9136";
9137
9138        let program = Program::parse(initial_source).unwrap().0.unwrap();
9139        let mut frontend = FrontendState::new();
9140        let mock_ctx = ExecutorContext::new_mock(None).await;
9141        let version = Version(0);
9142
9143        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9144        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9145        let sketch_id = sketch_object.id;
9146        let sketch = expect_sketch(sketch_object);
9147        let line2_end_id = *sketch.segments.get(4).unwrap();
9148
9149        let segments = vec![ExistingSegmentCtor {
9150            id: line2_end_id,
9151            ctor: SegmentCtor::Point(PointCtor {
9152                position: Point2d {
9153                    x: Expr::Var(Number {
9154                        value: 9.0,
9155                        units: NumericSuffix::None,
9156                    }),
9157                    y: Expr::Var(Number {
9158                        value: 10.0,
9159                        units: NumericSuffix::None,
9160                    }),
9161                },
9162            }),
9163        }];
9164        let (preview_source, preview_delta) = frontend
9165            .edit_segments_with_options(
9166                &mock_ctx,
9167                version,
9168                sketch_id,
9169                segments,
9170                EditSegmentsOptions {
9171                    anchor_segment_ids: Some(vec![line2_end_id]),
9172                    drag_anchors: Vec::new(),
9173                    constraint_label_edits: Vec::new(),
9174                    commit_solved_initial_guesses: false,
9175                },
9176            )
9177            .await
9178            .unwrap();
9179
9180        assert!(
9181            !preview_delta.exec_outcome.var_solutions.is_empty(),
9182            "preview solve should still solve and return geometry feedback"
9183        );
9184        assert!(
9185            preview_source
9186                .text
9187                .contains("line1 = line(start = [var 1, var 2], end = [var 1, var 2])")
9188        );
9189        assert!(
9190            preview_source
9191                .text
9192                .contains("line2 = line(start = [var 5, var 6], end = [var 9, var 10])")
9193        );
9194
9195        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9196        assert_eq!(mock_source.text, preview_source.text);
9197
9198        mock_ctx.close().await;
9199    }
9200
9201    #[tokio::test(flavor = "multi_thread")]
9202    async fn test_add_constraint_persists_solver_feedback_for_next_mock_execute() {
9203        let initial_source = "\
9204sketch(on = XY) {
9205  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
9206}
9207";
9208
9209        let program = Program::parse(initial_source).unwrap().0.unwrap();
9210        let mut frontend = FrontendState::new();
9211        let mock_ctx = ExecutorContext::new_mock(None).await;
9212        let version = Version(0);
9213
9214        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9215        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9216        let sketch_id = sketch_object.id;
9217        let sketch = expect_sketch(sketch_object);
9218        let line_end_id = *sketch.segments.get(1).unwrap();
9219
9220        let constraint = Constraint::Fixed(Fixed {
9221            points: vec![FixedPoint {
9222                point: line_end_id,
9223                position: Point2d {
9224                    x: Number {
9225                        value: 20.0,
9226                        units: NumericSuffix::Mm,
9227                    },
9228                    y: Number {
9229                        value: 0.0,
9230                        units: NumericSuffix::Mm,
9231                    },
9232                },
9233            }],
9234        });
9235        let (constraint_source, _) = frontend
9236            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9237            .await
9238            .unwrap();
9239
9240        assert!(
9241            constraint_source
9242                .text
9243                .contains("line1 = line(start = [var 0, var 0], end = [var 20, var 0])"),
9244            "{}",
9245            constraint_source.text
9246        );
9247        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9248        assert_eq!(mock_source.text, constraint_source.text);
9249
9250        mock_ctx.close().await;
9251    }
9252
9253    #[test]
9254    fn test_no_solver_feedback_preserves_original_source() {
9255        let initial_source = "\
9256@settings(defaultLengthUnit = in, kclVersion = 2.0)
9257cylinder = startSketchOn(XY)
9258    |> circle(center= [0, 0], radius= 22)
9259    |> extrude(length = 14)
9260";
9261        let mut frontend = FrontendState::new();
9262        frontend.program = Program::parse(initial_source).unwrap().0.unwrap();
9263        let outcome = ExecOutcome {
9264            variables: Default::default(),
9265            operations: Default::default(),
9266            artifact_graph: Default::default(),
9267            scene_objects: Default::default(),
9268            source_range_to_object: Default::default(),
9269            var_solutions: Default::default(),
9270            refactor_metadata: Default::default(),
9271            issues: Default::default(),
9272            filenames: Default::default(),
9273            source_files: Default::default(),
9274            default_planes: Default::default(),
9275        };
9276
9277        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9278
9279        assert_eq!(source_delta.text, initial_source);
9280    }
9281
9282    /// Explicit drag anchors should limit which edited points become temporary
9283    /// fixed constraints.
9284    #[tokio::test(flavor = "multi_thread")]
9285    async fn test_edit_segments_with_anchor_ids_limits_drag_fixed_constraints() {
9286        let initial_source = "\
9287sketch(on = XY) {
9288  point1 = point(at = [var 0mm, var 0mm])
9289  point2 = point(at = [var 0mm, var 0mm])
9290  coincident([point1, point2])
9291}
9292";
9293
9294        let program = Program::parse(initial_source).unwrap().0.unwrap();
9295        let mut frontend = FrontendState::new();
9296        let mock_ctx = ExecutorContext::new_mock(None).await;
9297        let version = Version(0);
9298
9299        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9300        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9301        let sketch_id = sketch_object.id;
9302        let sketch = expect_sketch(sketch_object);
9303        let point1_id = sketch.segments[0];
9304        let point2_id = sketch.segments[1];
9305
9306        let segments = vec![
9307            ExistingSegmentCtor {
9308                id: point1_id,
9309                ctor: SegmentCtor::Point(PointCtor {
9310                    position: point_expr_mm(10.0, 0.0),
9311                }),
9312            },
9313            ExistingSegmentCtor {
9314                id: point2_id,
9315                ctor: SegmentCtor::Point(PointCtor {
9316                    position: point_expr_mm(100.0, 0.0),
9317                }),
9318            },
9319        ];
9320        let (_, scene_delta) = frontend
9321            .edit_segments_with_options(
9322                &mock_ctx,
9323                version,
9324                sketch_id,
9325                segments,
9326                EditSegmentsOptions {
9327                    anchor_segment_ids: Some(vec![point1_id]),
9328                    drag_anchors: Vec::new(),
9329                    constraint_label_edits: Vec::new(),
9330                    commit_solved_initial_guesses: true,
9331                },
9332            )
9333            .await
9334            .unwrap();
9335
9336        assert_point_position_close(
9337            point_position(&scene_delta.new_graph, point1_id),
9338            point_number_mm(10.0, 0.0),
9339        );
9340        assert_point_position_close(
9341            point_position(&scene_delta.new_graph, point2_id),
9342            point_number_mm(10.0, 0.0),
9343        );
9344
9345        mock_ctx.close().await;
9346    }
9347
9348    /// Walks a program collecting `(literal_source_range, sketch_var_node_path)`
9349    /// for every SketchVar whose initial NumericLiteral has the given value.
9350    fn collect_sketch_var_literals_with_value(program: &Program, value: f64) -> Vec<(SourceRange, ast::NodePath)> {
9351        use std::cell::RefCell;
9352        struct Collector {
9353            target: f64,
9354            out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9355        }
9356        impl<'a> crate::walk::Visitor<'a> for &Collector {
9357            type Error = crate::front::Error;
9358            fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9359                if let crate::walk::Node::SketchVar(sketch_var) = node
9360                    && let (Some(initial), Some(node_path)) = (&sketch_var.initial, &sketch_var.node_path)
9361                    && (initial.value - self.target).abs() < 1e-9
9362                {
9363                    self.out
9364                        .borrow_mut()
9365                        .push((SourceRange::from(initial.as_ref()), node_path.clone()));
9366                }
9367                for child in node.children().iter() {
9368                    if !child.visit(*self)? {
9369                        return Ok(false);
9370                    }
9371                }
9372                Ok(true)
9373            }
9374        }
9375        let collector = Collector {
9376            target: value,
9377            out: Default::default(),
9378        };
9379        let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9380        collector.out.into_inner()
9381    }
9382
9383    /// Walk a program collecting `(sketch_var_source_range, sketch_var_node_path)`
9384    /// for every SketchVar (including bare `var`).
9385    fn collect_all_sketch_vars(program: &Program) -> Vec<(SourceRange, ast::NodePath)> {
9386        use std::cell::RefCell;
9387        struct Collector {
9388            out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9389        }
9390        impl<'a> crate::walk::Visitor<'a> for &Collector {
9391            type Error = crate::front::Error;
9392            fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9393                if let crate::walk::Node::SketchVar(sketch_var) = node
9394                    && let Some(node_path) = &sketch_var.node_path
9395                {
9396                    self.out
9397                        .borrow_mut()
9398                        .push((SourceRange::from(sketch_var), node_path.clone()));
9399                }
9400                for child in node.children().iter() {
9401                    if !child.visit(*self)? {
9402                        return Ok(false);
9403                    }
9404                }
9405                Ok(true)
9406            }
9407        }
9408        let collector = Collector {
9409            out: Default::default(),
9410        };
9411        let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9412        collector.out.into_inner()
9413    }
9414
9415    fn empty_exec_outcome_with_var_solutions(
9416        var_solutions: Vec<(SourceRange, Option<ast::NodePath>, Number)>,
9417    ) -> ExecOutcome {
9418        ExecOutcome {
9419            variables: Default::default(),
9420            operations: Default::default(),
9421            artifact_graph: Default::default(),
9422            scene_objects: Default::default(),
9423            source_range_to_object: Default::default(),
9424            var_solutions,
9425            refactor_metadata: Default::default(),
9426            issues: Default::default(),
9427            filenames: Default::default(),
9428            source_files: Default::default(),
9429            default_planes: Default::default(),
9430        }
9431    }
9432
9433    /// Happy path: commit a var solution to a `var N` inside a sketch block
9434    /// using a correct NodePath. Confirms the node-path code path produces the
9435    /// expected source mutation.
9436    #[test]
9437    fn test_commit_var_solution_by_node_path_updates_sketch_var() {
9438        let initial_source = "\
9439sketch(on = XY) {
9440  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9441}
9442";
9443        let program = Program::parse(initial_source).unwrap().0.unwrap();
9444        let matches = collect_sketch_var_literals_with_value(&program, 10.0);
9445        assert_eq!(matches.len(), 1, "expected exactly one `var 10mm`");
9446        let (literal_range, node_path) = matches.into_iter().next().unwrap();
9447
9448        let mut frontend = FrontendState::new();
9449        frontend.program = program;
9450
9451        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9452            literal_range,
9453            Some(node_path),
9454            Number {
9455                value: 25.0,
9456                units: NumericSuffix::Mm,
9457            },
9458        )]);
9459
9460        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9461
9462        insta::assert_snapshot!(
9463            "test_commit_var_solution_by_node_path_updates_sketch_var",
9464            source_delta.text
9465        );
9466    }
9467
9468    /// Whitespace inserted earlier in the source shifts the original SketchVar
9469    /// SourceRange. With NodePath propagation the commit should still target
9470    /// the right `var`. We simulate this by collecting node_paths against a
9471    /// "compact" source, then loading the frontend with a "padded" source
9472    /// (whose byte offsets differ), and feeding the original (now stale)
9473    /// source range plus the correct node_path back into the commit.
9474    #[test]
9475    fn test_commit_var_solution_survives_whitespace_shift_earlier_in_file() {
9476        let compact_source = "\
9477sketch(on = XY) {
9478  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9479}
9480";
9481        let padded_source = "\
9482// added comment\n// added comment\n\nsketch(on = XY) {
9483  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9484}
9485";
9486        let compact_program = Program::parse(compact_source).unwrap().0.unwrap();
9487        let padded_program = Program::parse(padded_source).unwrap().0.unwrap();
9488
9489        let compact_match = collect_sketch_var_literals_with_value(&compact_program, 10.0)
9490            .into_iter()
9491            .next()
9492            .expect("expected `var 10mm` in compact source");
9493        let padded_match = collect_sketch_var_literals_with_value(&padded_program, 10.0)
9494            .into_iter()
9495            .next()
9496            .expect("expected `var 10mm` in padded source");
9497
9498        assert_ne!(
9499            compact_match.0, padded_match.0,
9500            "byte offsets must differ for this test to be meaningful"
9501        );
9502        assert_eq!(
9503            compact_match.1, padded_match.1,
9504            "node paths must agree across whitespace; that's the whole point of NodePath",
9505        );
9506
9507        let mut frontend = FrontendState::new();
9508        frontend.program = padded_program;
9509
9510        // Stale source range from the compact source + correct node_path.
9511        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9512            compact_match.0,
9513            Some(compact_match.1),
9514            Number {
9515                value: 30.0,
9516                units: NumericSuffix::Mm,
9517            },
9518        )]);
9519
9520        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9521
9522        insta::assert_snapshot!(
9523            "test_commit_var_solution_survives_whitespace_shift_earlier_in_file",
9524            source_delta.text
9525        );
9526    }
9527
9528    /// When multiple `var` declarations exist and the stale source range
9529    /// happens to land on a *different* var, the node_path must take
9530    /// precedence and the right var gets updated.
9531    #[test]
9532    fn test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var() {
9533        let initial_source = "\
9534sketch(on = XY) {
9535  line1 = line(start = [var 10mm, var 0mm], end = [var 20mm, var 0mm])
9536}
9537";
9538        let program = Program::parse(initial_source).unwrap().0.unwrap();
9539
9540        let var_10 = collect_sketch_var_literals_with_value(&program, 10.0)
9541            .into_iter()
9542            .next()
9543            .expect("expected `var 10mm`");
9544        let var_20 = collect_sketch_var_literals_with_value(&program, 20.0)
9545            .into_iter()
9546            .next()
9547            .expect("expected `var 20mm`");
9548
9549        let mut frontend = FrontendState::new();
9550        frontend.program = program;
9551
9552        // Use var 20mm's source range, but var 10mm's node_path. node_path wins.
9553        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9554            var_20.0,
9555            Some(var_10.1),
9556            Number {
9557                value: 33.0,
9558                units: NumericSuffix::Mm,
9559            },
9560        )]);
9561
9562        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9563
9564        insta::assert_snapshot!(
9565            "test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var",
9566            source_delta.text
9567        );
9568    }
9569
9570    /// Bare `var` (no initial literal) is only locatable via node_path. With
9571    /// the EditVarInitialValue handler now operating on the SketchVar node, a
9572    /// solver solution should fill the initial value in. The
9573    /// `@settings(experimentalFeatures = allow)` is required because bare `var`
9574    /// is gated as an experimental feature; without it the re-parse of the
9575    /// recast source rejects bare `var` declarations.
9576    #[test]
9577    fn test_commit_var_solution_writes_back_into_bare_var() {
9578        let initial_source = "\
9579@settings(experimentalFeatures = allow, kclVersion = 2.0)
9580sketch(on = XY) {
9581  line1 = line(start = [var, var 0mm], end = [var 10mm, var 0])
9582}
9583";
9584        let program = Program::parse(initial_source).unwrap().0.unwrap();
9585
9586        // Pick the first bare `var`; collect_all_sketch_vars returns every
9587        // SketchVar, including bare ones.
9588        let bare = collect_all_sketch_vars(&program)
9589            .into_iter()
9590            .find(|(range, _)| {
9591                // The bare `var` is exactly the 3 characters "var".
9592                range.end() - range.start() == 3
9593            })
9594            .expect("expected at least one bare `var`");
9595
9596        let mut frontend = FrontendState::new();
9597        frontend.program = program;
9598
9599        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9600            bare.0,
9601            Some(bare.1),
9602            Number {
9603                value: 7.0,
9604                units: NumericSuffix::Mm,
9605            },
9606        )]);
9607
9608        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9609
9610        // Default length unit (mm; no `@settings(defaultLengthUnit = …)`) is
9611        // written as an explicit suffix so the bare var commits with units.
9612        // The recast adds a blank line after the `@settings` annotation.
9613        insta::assert_snapshot!("test_commit_var_solution_writes_back_into_bare_var", source_delta.text);
9614    }
9615
9616    #[tokio::test(flavor = "multi_thread")]
9617    async fn test_delete_point_without_var() {
9618        let initial_source = "\
9619sketch(on = XY) {
9620  point(at = [var 1, var 2])
9621  point(at = [var 3, var 4])
9622  point(at = [var 5, var 6])
9623}
9624";
9625
9626        let program = Program::parse(initial_source).unwrap().0.unwrap();
9627
9628        let mut frontend = FrontendState::new();
9629
9630        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9631        let mock_ctx = ExecutorContext::new_mock(None).await;
9632        let version = Version(0);
9633
9634        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9635        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9636        let sketch_id = sketch_object.id;
9637        let sketch = expect_sketch(sketch_object);
9638
9639        let point_id = *sketch.segments.get(1).unwrap();
9640
9641        let (src_delta, scene_delta) = frontend
9642            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9643            .await
9644            .unwrap();
9645        insta::assert_snapshot!("test_delete_point_without_var", src_delta.text.as_str());
9646        assert_eq!(scene_delta.new_objects, vec![]);
9647        assert_eq!(scene_delta.new_graph.objects.len(), 4);
9648
9649        ctx.close().await;
9650        mock_ctx.close().await;
9651    }
9652
9653    #[tokio::test(flavor = "multi_thread")]
9654    async fn test_delete_point_with_var() {
9655        let initial_source = "\
9656sketch(on = XY) {
9657  point(at = [var 1, var 2])
9658  point1 = point(at = [var 3, var 4])
9659  point(at = [var 5, var 6])
9660}
9661";
9662
9663        let program = Program::parse(initial_source).unwrap().0.unwrap();
9664
9665        let mut frontend = FrontendState::new();
9666
9667        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9668        let mock_ctx = ExecutorContext::new_mock(None).await;
9669        let version = Version(0);
9670
9671        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9672        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9673        let sketch_id = sketch_object.id;
9674        let sketch = expect_sketch(sketch_object);
9675
9676        let point_id = *sketch.segments.get(1).unwrap();
9677
9678        let (src_delta, scene_delta) = frontend
9679            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9680            .await
9681            .unwrap();
9682        insta::assert_snapshot!("test_delete_point_with_var", src_delta.text.as_str());
9683        assert_eq!(scene_delta.new_objects, vec![]);
9684        assert_eq!(scene_delta.new_graph.objects.len(), 4);
9685
9686        ctx.close().await;
9687        mock_ctx.close().await;
9688    }
9689
9690    #[tokio::test(flavor = "multi_thread")]
9691    async fn test_delete_multiple_points() {
9692        let initial_source = "\
9693sketch(on = XY) {
9694  point(at = [var 1, var 2])
9695  point1 = point(at = [var 3, var 4])
9696  point(at = [var 5, var 6])
9697}
9698";
9699
9700        let program = Program::parse(initial_source).unwrap().0.unwrap();
9701
9702        let mut frontend = FrontendState::new();
9703
9704        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9705        let mock_ctx = ExecutorContext::new_mock(None).await;
9706        let version = Version(0);
9707
9708        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9709        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9710        let sketch_id = sketch_object.id;
9711
9712        let sketch = expect_sketch(sketch_object);
9713
9714        let point1_id = *sketch.segments.first().unwrap();
9715        let point2_id = *sketch.segments.get(1).unwrap();
9716
9717        let (src_delta, scene_delta) = frontend
9718            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
9719            .await
9720            .unwrap();
9721        insta::assert_snapshot!("test_delete_multiple_points", src_delta.text.as_str());
9722        assert_eq!(scene_delta.new_objects, vec![]);
9723        assert_eq!(scene_delta.new_graph.objects.len(), 3);
9724
9725        ctx.close().await;
9726        mock_ctx.close().await;
9727    }
9728
9729    #[tokio::test(flavor = "multi_thread")]
9730    async fn test_delete_coincident_constraint() {
9731        let initial_source = "\
9732sketch(on = XY) {
9733  point1 = point(at = [var 1, var 2])
9734  point2 = point(at = [var 3, var 4])
9735  coincident([point1, point2])
9736  point(at = [var 5, var 6])
9737}
9738";
9739
9740        let program = Program::parse(initial_source).unwrap().0.unwrap();
9741
9742        let mut frontend = FrontendState::new();
9743
9744        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9745        let mock_ctx = ExecutorContext::new_mock(None).await;
9746        let version = Version(0);
9747
9748        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9749        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9750        let sketch_id = sketch_object.id;
9751        let sketch = expect_sketch(sketch_object);
9752
9753        let coincident_id = *sketch.constraints.first().unwrap();
9754
9755        let (src_delta, scene_delta) = frontend
9756            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
9757            .await
9758            .unwrap();
9759        insta::assert_snapshot!("test_delete_coincident_constraint", src_delta.text.as_str());
9760        assert_eq!(scene_delta.new_objects, vec![]);
9761        assert_eq!(scene_delta.new_graph.objects.len(), 5);
9762
9763        ctx.close().await;
9764        mock_ctx.close().await;
9765    }
9766
9767    #[tokio::test(flavor = "multi_thread")]
9768    async fn test_delete_line_cascades_to_coincident_constraint() {
9769        let initial_source = "\
9770sketch(on = XY) {
9771  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9772  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9773  coincident([line1.end, line2.start])
9774}
9775";
9776
9777        let program = Program::parse(initial_source).unwrap().0.unwrap();
9778
9779        let mut frontend = FrontendState::new();
9780
9781        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9782        let mock_ctx = ExecutorContext::new_mock(None).await;
9783        let version = Version(0);
9784
9785        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9786        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9787        let sketch_id = sketch_object.id;
9788        let sketch = expect_sketch(sketch_object);
9789        let line_id = *sketch.segments.get(5).unwrap();
9790
9791        let (src_delta, scene_delta) = frontend
9792            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9793            .await
9794            .unwrap();
9795        insta::assert_snapshot!(
9796            "test_delete_line_cascades_to_coincident_constraint",
9797            src_delta.text.as_str()
9798        );
9799        assert_eq!(
9800            scene_delta.new_graph.objects.len(),
9801            5,
9802            "{:#?}",
9803            scene_delta.new_graph.objects
9804        );
9805
9806        ctx.close().await;
9807        mock_ctx.close().await;
9808    }
9809
9810    #[tokio::test(flavor = "multi_thread")]
9811    async fn test_delete_line_cascades_to_distance_constraint() {
9812        let initial_source = "\
9813sketch(on = XY) {
9814  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9815  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9816  distance([line1.end, line2.start]) == 10mm
9817}
9818";
9819
9820        let program = Program::parse(initial_source).unwrap().0.unwrap();
9821
9822        let mut frontend = FrontendState::new();
9823
9824        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9825        let mock_ctx = ExecutorContext::new_mock(None).await;
9826        let version = Version(0);
9827
9828        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9829        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9830        let sketch_id = sketch_object.id;
9831        let sketch = expect_sketch(sketch_object);
9832        let line_id = *sketch.segments.get(5).unwrap();
9833
9834        let (src_delta, scene_delta) = frontend
9835            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9836            .await
9837            .unwrap();
9838        insta::assert_snapshot!(
9839            "test_delete_line_cascades_to_distance_constraint",
9840            src_delta.text.as_str()
9841        );
9842        assert_eq!(
9843            scene_delta.new_graph.objects.len(),
9844            5,
9845            "{:#?}",
9846            scene_delta.new_graph.objects
9847        );
9848
9849        ctx.close().await;
9850        mock_ctx.close().await;
9851    }
9852
9853    #[tokio::test(flavor = "multi_thread")]
9854    async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
9855        let initial_source = "\
9856sketch(on = XY) {
9857  point1 = point(at = [var 1, var 2])
9858  point2 = point(at = [var 3, var 4])
9859  horizontalDistance([point1, point2]) == 10mm
9860}
9861";
9862
9863        let program = Program::parse(initial_source).unwrap().0.unwrap();
9864
9865        let mut frontend = FrontendState::new();
9866
9867        let mock_ctx = ExecutorContext::new_mock(None).await;
9868        let version = Version(0);
9869
9870        frontend.program = program.clone();
9871        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9872        frontend.update_state_after_exec(outcome, true);
9873        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9874        let sketch_id = sketch_object.id;
9875        let sketch = expect_sketch(sketch_object);
9876        let point2_id = *sketch.segments.get(1).unwrap();
9877
9878        let (src_delta, scene_delta) = frontend
9879            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
9880            .await
9881            .unwrap();
9882        insta::assert_snapshot!(
9883            "test_delete_point_cascades_to_horizontal_distance_constraint",
9884            src_delta.text.as_str()
9885        );
9886        assert_eq!(
9887            scene_delta.new_graph.objects.len(),
9888            3,
9889            "{:#?}",
9890            scene_delta.new_graph.objects
9891        );
9892
9893        mock_ctx.close().await;
9894    }
9895
9896    #[tokio::test(flavor = "multi_thread")]
9897    async fn test_delete_line_cascades_to_fixed_constraint() {
9898        let initial_source = "\
9899sketch(on = XY) {
9900  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9901  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9902  fixed([line1.start, [0, 0]])
9903}
9904";
9905
9906        let program = Program::parse(initial_source).unwrap().0.unwrap();
9907
9908        let mut frontend = FrontendState::new();
9909
9910        let mock_ctx = ExecutorContext::new_mock(None).await;
9911        let version = Version(0);
9912
9913        frontend.program = program.clone();
9914        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9915        frontend.update_state_after_exec(outcome, true);
9916        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9917        let sketch_id = sketch_object.id;
9918        let sketch = expect_sketch(sketch_object);
9919        let line1_id = *sketch.segments.get(2).unwrap();
9920
9921        let (src_delta, scene_delta) = frontend
9922            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9923            .await
9924            .unwrap();
9925        insta::assert_snapshot!("test_delete_line_cascades_to_fixed_constraint", src_delta.text.as_str());
9926        assert_eq!(
9927            scene_delta.new_graph.objects.len(),
9928            5,
9929            "{:#?}",
9930            scene_delta.new_graph.objects
9931        );
9932
9933        mock_ctx.close().await;
9934    }
9935
9936    #[tokio::test(flavor = "multi_thread")]
9937    async fn test_delete_line_cascades_to_midpoint_constraint() {
9938        let initial_source = "\
9939sketch(on = XY) {
9940  point1 = point(at = [var 1, var 2])
9941  line1 = line(start = [var 0, var 0], end = [var 6, var 4])
9942  midpoint(line1, point = point1)
9943}
9944";
9945
9946        let program = Program::parse(initial_source).unwrap().0.unwrap();
9947
9948        let mut frontend = FrontendState::new();
9949
9950        let mock_ctx = ExecutorContext::new_mock(None).await;
9951        let version = Version(0);
9952
9953        frontend.program = program.clone();
9954        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9955        frontend.update_state_after_exec(outcome, true);
9956        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9957        let sketch_id = sketch_object.id;
9958        let sketch = expect_sketch(sketch_object);
9959        let line1_id = *sketch.segments.get(3).unwrap();
9960
9961        let (src_delta, scene_delta) = frontend
9962            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9963            .await
9964            .unwrap();
9965        insta::assert_snapshot!(
9966            "test_delete_line_cascades_to_midpoint_constraint",
9967            src_delta.text.as_str()
9968        );
9969        assert_eq!(
9970            scene_delta.new_graph.objects.len(),
9971            3,
9972            "{:#?}",
9973            scene_delta.new_graph.objects
9974        );
9975
9976        mock_ctx.close().await;
9977    }
9978
9979    #[tokio::test(flavor = "multi_thread")]
9980    async fn test_delete_point_preserves_multiline_coincident_constraint() {
9981        let initial_source = "\
9982sketch(on = XY) {
9983  point1 = point(at = [var 1, var 2])
9984  point2 = point(at = [var 3, var 4])
9985  point3 = point(at = [var 5, var 6])
9986  coincident([point1, point2, point3])
9987}
9988";
9989
9990        let program = Program::parse(initial_source).unwrap().0.unwrap();
9991
9992        let mut frontend = FrontendState::new();
9993
9994        let mock_ctx = ExecutorContext::new_mock(None).await;
9995        let version = Version(0);
9996
9997        frontend.program = program.clone();
9998        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9999        frontend.update_state_after_exec(outcome, true);
10000        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10001        let sketch_id = sketch_object.id;
10002        let sketch = expect_sketch(sketch_object);
10003        let point3_id = *sketch.segments.get(2).unwrap();
10004
10005        let (src_delta, scene_delta) = frontend
10006            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
10007            .await
10008            .unwrap();
10009        assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
10010        assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
10011        assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
10012        assert!(
10013            src_delta.text.contains("coincident([point1, point2])"),
10014            "{}",
10015            src_delta.text
10016        );
10017
10018        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10019        let sketch = expect_sketch(sketch_object);
10020        assert_eq!(sketch.segments.len(), 2);
10021        assert_eq!(sketch.constraints.len(), 1);
10022
10023        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10024        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10025            panic!("Expected constraint object");
10026        };
10027        let Constraint::Coincident(coincident) = constraint else {
10028            panic!("Expected coincident constraint");
10029        };
10030        assert_eq!(
10031            coincident.segments,
10032            sketch
10033                .segments
10034                .iter()
10035                .copied()
10036                .map(Into::into)
10037                .collect::<Vec<ConstraintSegment>>()
10038        );
10039
10040        mock_ctx.close().await;
10041    }
10042
10043    #[tokio::test(flavor = "multi_thread")]
10044    async fn test_delete_line_preserves_multiline_equal_length_constraint() {
10045        let initial_source = "\
10046sketch(on = XY) {
10047  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10048  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10049  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10050  equalLength([line1, line2, line3])
10051}
10052";
10053
10054        let program = Program::parse(initial_source).unwrap().0.unwrap();
10055
10056        let mut frontend = FrontendState::new();
10057
10058        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10059        let mock_ctx = ExecutorContext::new_mock(None).await;
10060        let version = Version(0);
10061
10062        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10063        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10064        let sketch_id = sketch_object.id;
10065        let sketch = expect_sketch(sketch_object);
10066        let line3_id = *sketch.segments.get(8).unwrap();
10067
10068        let (src_delta, scene_delta) = frontend
10069            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10070            .await
10071            .unwrap();
10072        insta::assert_snapshot!(
10073            "test_delete_line_preserves_multiline_equal_length_constraint",
10074            src_delta.text.as_str()
10075        );
10076
10077        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10078        let sketch = expect_sketch(sketch_object);
10079        assert_eq!(sketch.constraints.len(), 1);
10080
10081        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10082        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10083            panic!("Expected constraint object");
10084        };
10085        let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
10086            panic!("Expected lines equal length constraint");
10087        };
10088        assert_eq!(lines_equal_length.lines.len(), 2);
10089
10090        ctx.close().await;
10091        mock_ctx.close().await;
10092    }
10093
10094    #[tokio::test(flavor = "multi_thread")]
10095    async fn test_delete_line_preserves_multiline_horizontal_constraint() {
10096        let initial_source = "\
10097sketch(on = XY) {
10098  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10099  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10100  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10101  horizontal([line1.end, line2.start, line3.start])
10102}
10103";
10104
10105        let program = Program::parse(initial_source).unwrap().0.unwrap();
10106
10107        let mut frontend = FrontendState::new();
10108
10109        let mock_ctx = ExecutorContext::new_mock(None).await;
10110        let version = Version(0);
10111
10112        frontend.program = program.clone();
10113        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10114        frontend.update_state_after_exec(outcome, true);
10115        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10116        let sketch_id = sketch_object.id;
10117        let sketch = expect_sketch(sketch_object);
10118        let line1_id = *sketch.segments.get(2).unwrap();
10119
10120        let (src_delta, scene_delta) = frontend
10121            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10122            .await
10123            .unwrap();
10124        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10125        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10126        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10127        assert!(
10128            src_delta.text.contains("horizontal([line2.start, line3.start])"),
10129            "{}",
10130            src_delta.text
10131        );
10132
10133        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10134        let sketch = expect_sketch(sketch_object);
10135        assert_eq!(sketch.constraints.len(), 1);
10136
10137        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10138        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10139            panic!("Expected constraint object");
10140        };
10141        let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
10142            panic!("Expected horizontal points constraint");
10143        };
10144        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10145        assert_eq!(*points, remaining_points);
10146
10147        mock_ctx.close().await;
10148    }
10149
10150    #[tokio::test(flavor = "multi_thread")]
10151    async fn test_delete_line_preserves_multiline_vertical_constraint() {
10152        let initial_source = "\
10153sketch(on = XY) {
10154  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10155  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10156  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10157  vertical([line1.end, line2.start, line3.start])
10158}
10159";
10160
10161        let program = Program::parse(initial_source).unwrap().0.unwrap();
10162
10163        let mut frontend = FrontendState::new();
10164
10165        let mock_ctx = ExecutorContext::new_mock(None).await;
10166        let version = Version(0);
10167
10168        frontend.program = program.clone();
10169        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10170        frontend.update_state_after_exec(outcome, true);
10171        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10172        let sketch_id = sketch_object.id;
10173        let sketch = expect_sketch(sketch_object);
10174        let line1_id = *sketch.segments.get(2).unwrap();
10175
10176        let (src_delta, scene_delta) = frontend
10177            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10178            .await
10179            .unwrap();
10180        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10181        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10182        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10183        assert!(
10184            src_delta.text.contains("vertical([line2.start, line3.start])"),
10185            "{}",
10186            src_delta.text
10187        );
10188
10189        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10190        let sketch = expect_sketch(sketch_object);
10191        assert_eq!(sketch.constraints.len(), 1);
10192
10193        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10194        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10195            panic!("Expected constraint object");
10196        };
10197        let Constraint::Vertical(Vertical::Points { points }) = constraint else {
10198            panic!("Expected vertical points constraint");
10199        };
10200        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10201        assert_eq!(*points, remaining_points);
10202
10203        mock_ctx.close().await;
10204    }
10205
10206    #[tokio::test(flavor = "multi_thread")]
10207    async fn test_delete_line_preserves_multiline_coincident_constraint() {
10208        let initial_source = "\
10209sketch(on = XY) {
10210  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10211  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10212  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10213  coincident([line1.end, line2.start, line3.start])
10214}
10215";
10216
10217        let program = Program::parse(initial_source).unwrap().0.unwrap();
10218
10219        let mut frontend = FrontendState::new();
10220
10221        let mock_ctx = ExecutorContext::new_mock(None).await;
10222        let version = Version(0);
10223
10224        frontend.program = program.clone();
10225        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10226        frontend.update_state_after_exec(outcome, true);
10227        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10228        let sketch_id = sketch_object.id;
10229        let sketch = expect_sketch(sketch_object);
10230        let line1_id = *sketch.segments.get(2).unwrap();
10231
10232        let (src_delta, scene_delta) = frontend
10233            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10234            .await
10235            .unwrap();
10236        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10237        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10238        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10239        assert!(
10240            src_delta.text.contains("coincident([line2.start, line3.start])"),
10241            "{}",
10242            src_delta.text
10243        );
10244
10245        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10246        let sketch = expect_sketch(sketch_object);
10247        assert_eq!(sketch.constraints.len(), 1);
10248
10249        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10250        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10251            panic!("Expected constraint object");
10252        };
10253        let Constraint::Coincident(coincident) = constraint else {
10254            panic!("Expected coincident constraint");
10255        };
10256        let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10257        assert_eq!(coincident.segments, remaining_segments);
10258
10259        mock_ctx.close().await;
10260    }
10261
10262    #[tokio::test(flavor = "multi_thread")]
10263    async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
10264        let initial_source = "\
10265sketch(on = XY) {
10266  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10267  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10268  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10269  equalLength([line1, line2, line3])
10270}
10271";
10272
10273        let program = Program::parse(initial_source).unwrap().0.unwrap();
10274
10275        let mut frontend = FrontendState::new();
10276
10277        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10278        let mock_ctx = ExecutorContext::new_mock(None).await;
10279        let version = Version(0);
10280
10281        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10282        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10283        let sketch_id = sketch_object.id;
10284        let sketch = expect_sketch(sketch_object);
10285        let line2_id = *sketch.segments.get(5).unwrap();
10286        let line3_id = *sketch.segments.get(8).unwrap();
10287
10288        let (src_delta, scene_delta) = frontend
10289            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10290            .await
10291            .unwrap();
10292        insta::assert_snapshot!(
10293            "test_delete_lines_removes_multiline_equal_length_constraint_below_minimum",
10294            src_delta.text.as_str()
10295        );
10296
10297        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10298        let sketch = expect_sketch(sketch_object);
10299        assert!(sketch.constraints.is_empty());
10300
10301        ctx.close().await;
10302        mock_ctx.close().await;
10303    }
10304
10305    #[tokio::test(flavor = "multi_thread")]
10306    async fn test_delete_line_preserves_multiline_parallel_constraint() {
10307        let initial_source = "\
10308sketch(on = XY) {
10309  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10310  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10311  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10312  parallel([line1, line2, line3])
10313}
10314";
10315
10316        let program = Program::parse(initial_source).unwrap().0.unwrap();
10317
10318        let mut frontend = FrontendState::new();
10319
10320        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10321        let mock_ctx = ExecutorContext::new_mock(None).await;
10322        let version = Version(0);
10323
10324        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10325        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10326        let sketch_id = sketch_object.id;
10327        let sketch = expect_sketch(sketch_object);
10328        let line3_id = *sketch.segments.get(8).unwrap();
10329
10330        let (src_delta, scene_delta) = frontend
10331            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10332            .await
10333            .unwrap();
10334        insta::assert_snapshot!(
10335            "test_delete_line_preserves_multiline_parallel_constraint",
10336            src_delta.text.as_str()
10337        );
10338
10339        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10340        let sketch = expect_sketch(sketch_object);
10341        assert_eq!(sketch.constraints.len(), 1);
10342
10343        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10344        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10345            panic!("Expected constraint object");
10346        };
10347        let Constraint::Parallel(parallel) = constraint else {
10348            panic!("Expected parallel constraint");
10349        };
10350        assert_eq!(parallel.lines.len(), 2);
10351
10352        ctx.close().await;
10353        mock_ctx.close().await;
10354    }
10355
10356    #[tokio::test(flavor = "multi_thread")]
10357    async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
10358        let initial_source = "\
10359sketch(on = XY) {
10360  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10361  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10362  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10363  parallel([line1, line2, line3])
10364}
10365";
10366
10367        let program = Program::parse(initial_source).unwrap().0.unwrap();
10368
10369        let mut frontend = FrontendState::new();
10370
10371        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10372        let mock_ctx = ExecutorContext::new_mock(None).await;
10373        let version = Version(0);
10374
10375        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10376        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10377        let sketch_id = sketch_object.id;
10378        let sketch = expect_sketch(sketch_object);
10379        let line2_id = *sketch.segments.get(5).unwrap();
10380        let line3_id = *sketch.segments.get(8).unwrap();
10381
10382        let (src_delta, scene_delta) = frontend
10383            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10384            .await
10385            .unwrap();
10386        insta::assert_snapshot!(
10387            "test_delete_lines_removes_multiline_parallel_constraint_below_minimum",
10388            src_delta.text.as_str()
10389        );
10390
10391        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10392        let sketch = expect_sketch(sketch_object);
10393        assert!(sketch.constraints.is_empty());
10394
10395        ctx.close().await;
10396        mock_ctx.close().await;
10397    }
10398
10399    #[tokio::test(flavor = "multi_thread")]
10400    async fn test_delete_line_line_coincident_constraint() {
10401        let initial_source = "\
10402sketch(on = XY) {
10403  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10404  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10405  coincident([line1, line2])
10406}
10407";
10408
10409        let program = Program::parse(initial_source).unwrap().0.unwrap();
10410
10411        let mut frontend = FrontendState::new();
10412
10413        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10414        let mock_ctx = ExecutorContext::new_mock(None).await;
10415        let version = Version(0);
10416
10417        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10418        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10419        let sketch_id = sketch_object.id;
10420        let sketch = expect_sketch(sketch_object);
10421
10422        let coincident_id = *sketch.constraints.first().unwrap();
10423
10424        let (src_delta, scene_delta) = frontend
10425            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
10426            .await
10427            .unwrap();
10428        insta::assert_snapshot!("test_delete_line_line_coincident_constraint", src_delta.text.as_str());
10429        assert_eq!(scene_delta.new_objects, vec![]);
10430        assert_eq!(scene_delta.new_graph.objects.len(), 8);
10431
10432        ctx.close().await;
10433        mock_ctx.close().await;
10434    }
10435
10436    #[tokio::test(flavor = "multi_thread")]
10437    async fn test_two_points_coincident() {
10438        let initial_source = "\
10439sketch(on = XY) {
10440  point1 = point(at = [var 1, var 2])
10441  point(at = [3, 4])
10442}
10443";
10444
10445        let program = Program::parse(initial_source).unwrap().0.unwrap();
10446
10447        let mut frontend = FrontendState::new();
10448
10449        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10450        let mock_ctx = ExecutorContext::new_mock(None).await;
10451        let version = Version(0);
10452
10453        frontend.hack_set_program(&ctx, program).await.unwrap();
10454        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10455        let sketch_id = sketch_object.id;
10456        let sketch = expect_sketch(sketch_object);
10457        let point0_id = *sketch.segments.first().unwrap();
10458        let point1_id = *sketch.segments.get(1).unwrap();
10459
10460        let constraint = Constraint::Coincident(Coincident {
10461            segments: vec![point0_id.into(), point1_id.into()],
10462        });
10463        let (src_delta, scene_delta) = frontend
10464            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10465            .await
10466            .unwrap();
10467        insta::assert_snapshot!("test_two_points_coincident", src_delta.text.as_str());
10468        assert_eq!(
10469            scene_delta.new_graph.objects.len(),
10470            5,
10471            "{:#?}",
10472            scene_delta.new_graph.objects
10473        );
10474
10475        ctx.close().await;
10476        mock_ctx.close().await;
10477    }
10478
10479    #[tokio::test(flavor = "multi_thread")]
10480    async fn test_three_points_coincident() {
10481        let initial_source = "\
10482sketch(on = XY) {
10483  point1 = point(at = [var 1, var 2])
10484  point(at = [var 3, var 4])
10485  point(at = [var 5, var 6])
10486}
10487";
10488
10489        let program = Program::parse(initial_source).unwrap().0.unwrap();
10490
10491        let mut frontend = FrontendState::new();
10492
10493        let mock_ctx = ExecutorContext::new_mock(None).await;
10494        let version = Version(0);
10495
10496        frontend.program = program.clone();
10497        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10498        frontend.update_state_after_exec(outcome, true);
10499        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10500        let sketch_id = sketch_object.id;
10501        let sketch = expect_sketch(sketch_object);
10502        let segments = sketch
10503            .segments
10504            .iter()
10505            .take(3)
10506            .copied()
10507            .map(Into::into)
10508            .collect::<Vec<ConstraintSegment>>();
10509
10510        let constraint = Constraint::Coincident(Coincident {
10511            segments: segments.clone(),
10512        });
10513        let (src_delta, scene_delta) = frontend
10514            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10515            .await
10516            .unwrap();
10517        insta::assert_snapshot!("test_three_points_coincident", src_delta.text.as_str());
10518
10519        let constraint_object = scene_delta
10520            .new_graph
10521            .objects
10522            .iter()
10523            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10524            .unwrap();
10525
10526        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10527            panic!("expected a constraint object");
10528        };
10529
10530        assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10531
10532        mock_ctx.close().await;
10533    }
10534
10535    #[tokio::test(flavor = "multi_thread")]
10536    async fn test_source_with_three_point_coincident_tracks_all_segments() {
10537        let initial_source = "\
10538sketch(on = XY) {
10539  point1 = point(at = [var 1, var 2])
10540  point2 = point(at = [var 3, var 4])
10541  point3 = point(at = [var 5, var 6])
10542  coincident([point1, point2, point3])
10543}
10544";
10545
10546        let program = Program::parse(initial_source).unwrap().0.unwrap();
10547
10548        let mut frontend = FrontendState::new();
10549
10550        let ctx = ExecutorContext::new_mock(None).await;
10551        frontend.program = program.clone();
10552        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10553        frontend.update_state_after_exec(outcome, true);
10554
10555        let constraint_object = frontend
10556            .scene_graph
10557            .objects
10558            .iter()
10559            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10560            .unwrap();
10561        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10562            panic!("expected a constraint object");
10563        };
10564
10565        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10566        let sketch = expect_sketch(sketch_object);
10567        let expected_segments = sketch
10568            .segments
10569            .iter()
10570            .take(3)
10571            .copied()
10572            .map(Into::into)
10573            .collect::<Vec<ConstraintSegment>>();
10574
10575        assert_eq!(
10576            constraint,
10577            &Constraint::Coincident(Coincident {
10578                segments: expected_segments,
10579            })
10580        );
10581
10582        ctx.close().await;
10583    }
10584
10585    #[tokio::test(flavor = "multi_thread")]
10586    async fn test_point_origin_coincident_preserves_order() {
10587        let initial_source = "\
10588sketch(on = XY) {
10589  point(at = [var 1, var 2])
10590}
10591";
10592
10593        for (origin_first, snapshot_name) in [
10594            (true, "test_point_origin_coincident_preserves_order_origin_first"),
10595            (false, "test_point_origin_coincident_preserves_order_point_first"),
10596        ] {
10597            let program = Program::parse(initial_source).unwrap().0.unwrap();
10598
10599            let mut frontend = FrontendState::new();
10600
10601            let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10602            let mock_ctx = ExecutorContext::new_mock(None).await;
10603            let version = Version(0);
10604
10605            frontend.hack_set_program(&ctx, program).await.unwrap();
10606            let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10607            let sketch_id = sketch_object.id;
10608            let sketch = expect_sketch(sketch_object);
10609            let point_id = *sketch.segments.first().unwrap();
10610
10611            let segments = if origin_first {
10612                vec![ConstraintSegment::ORIGIN, point_id.into()]
10613            } else {
10614                vec![point_id.into(), ConstraintSegment::ORIGIN]
10615            };
10616            let constraint = Constraint::Coincident(Coincident {
10617                segments: segments.clone(),
10618            });
10619            let (src_delta, scene_delta) = frontend
10620                .add_constraint(&mock_ctx, version, sketch_id, constraint)
10621                .await
10622                .unwrap();
10623            insta::assert_snapshot!(snapshot_name, src_delta.text.as_str());
10624
10625            let constraint_object = scene_delta
10626                .new_graph
10627                .objects
10628                .iter()
10629                .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10630                .unwrap();
10631
10632            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10633                panic!("expected a constraint object");
10634            };
10635
10636            assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10637
10638            ctx.close().await;
10639            mock_ctx.close().await;
10640        }
10641    }
10642
10643    #[tokio::test(flavor = "multi_thread")]
10644    async fn test_coincident_of_line_end_points() {
10645        let initial_source = "\
10646sketch(on = XY) {
10647  line(start = [var 1, var 2], end = [var 3, var 4])
10648  line(start = [var 5, var 6], end = [var 7, var 8])
10649}
10650";
10651
10652        let program = Program::parse(initial_source).unwrap().0.unwrap();
10653
10654        let mut frontend = FrontendState::new();
10655
10656        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10657        let mock_ctx = ExecutorContext::new_mock(None).await;
10658        let version = Version(0);
10659
10660        frontend.hack_set_program(&ctx, program).await.unwrap();
10661        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10662        let sketch_id = sketch_object.id;
10663        let sketch = expect_sketch(sketch_object);
10664        let point0_id = *sketch.segments.get(1).unwrap();
10665        let point1_id = *sketch.segments.get(3).unwrap();
10666
10667        let constraint = Constraint::Coincident(Coincident {
10668            segments: vec![point0_id.into(), point1_id.into()],
10669        });
10670        let (src_delta, scene_delta) = frontend
10671            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10672            .await
10673            .unwrap();
10674        insta::assert_snapshot!("test_coincident_of_line_end_points", src_delta.text.as_str());
10675        assert_eq!(
10676            scene_delta.new_graph.objects.len(),
10677            9,
10678            "{:#?}",
10679            scene_delta.new_graph.objects
10680        );
10681
10682        ctx.close().await;
10683        mock_ctx.close().await;
10684    }
10685
10686    #[tokio::test(flavor = "multi_thread")]
10687    async fn test_coincident_of_line_point_and_circle_segment() {
10688        let initial_source = "\
10689sketch(on = XY) {
10690  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
10691  line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
10692}
10693";
10694        let program = Program::parse(initial_source).unwrap().0.unwrap();
10695        let mut frontend = FrontendState::new();
10696
10697        let mock_ctx = ExecutorContext::new_mock(None).await;
10698        let version = Version(0);
10699
10700        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10701        frontend.program = program;
10702        frontend.update_state_after_exec(outcome, true);
10703        let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
10704        let sketch_id = sketch_object.id;
10705        let sketch = expect_sketch(sketch_object);
10706
10707        let circle_id = sketch
10708            .segments
10709            .iter()
10710            .copied()
10711            .find(|seg_id| {
10712                matches!(
10713                    &frontend.scene_graph.objects[seg_id.0].kind,
10714                    ObjectKind::Segment {
10715                        segment: Segment::Circle(_)
10716                    }
10717                )
10718            })
10719            .expect("Expected a circle segment in sketch");
10720        let line_id = frontend
10721            .scene_graph
10722            .objects
10723            .iter()
10724            .find_map(|obj| match &obj.kind {
10725                ObjectKind::Segment {
10726                    segment: Segment::Line(line),
10727                } if line.owner.is_none() => Some(obj.id),
10728                _ => None,
10729            })
10730            .expect("Expected a standalone line segment in scene graph");
10731
10732        let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
10733            ObjectKind::Segment {
10734                segment: Segment::Line(line),
10735            } => line.start,
10736            _ => panic!("Expected line segment object"),
10737        };
10738
10739        let constraint = Constraint::Coincident(Coincident {
10740            segments: vec![line_start_point_id.into(), circle_id.into()],
10741        });
10742        let (src_delta, _scene_delta) = frontend
10743            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10744            .await
10745            .unwrap();
10746        insta::assert_snapshot!(
10747            "test_coincident_of_line_point_and_circle_segment",
10748            src_delta.text.as_str()
10749        );
10750
10751        mock_ctx.close().await;
10752    }
10753
10754    #[tokio::test(flavor = "multi_thread")]
10755    async fn test_invalid_coincident_arc_and_line_preserves_state() {
10756        // Test that attempting an invalid coincident constraint (arc and line)
10757        // doesn't corrupt the state, allowing subsequent operations to work.
10758        // This test verifies the transactional fix in add_constraint that prevents
10759        // state corruption when invalid constraints are attempted.
10760        // Example: coincident constraint between an arc segment and a straight line segment
10761        // is geometrically invalid and should fail, but state should remain intact.
10762        // Use the programmatic approach (new_sketch + add_segment) like test_new_sketch_add_arc_edit_arc
10763        let program = Program::empty();
10764
10765        let mut frontend = FrontendState::new();
10766        frontend.program = program;
10767
10768        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10769        let mock_ctx = ExecutorContext::new_mock(None).await;
10770        let version = Version(0);
10771
10772        let sketch_args = SketchCtor {
10773            on: Plane::Default(PlaneName::Xy),
10774        };
10775        let (_src_delta, _scene_delta, sketch_id) = frontend
10776            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10777            .await
10778            .unwrap();
10779
10780        // Add an arc segment
10781        let arc_ctor = ArcCtor {
10782            start: Point2d {
10783                x: Expr::Var(Number {
10784                    value: 0.0,
10785                    units: NumericSuffix::Mm,
10786                }),
10787                y: Expr::Var(Number {
10788                    value: 0.0,
10789                    units: NumericSuffix::Mm,
10790                }),
10791            },
10792            end: Point2d {
10793                x: Expr::Var(Number {
10794                    value: 10.0,
10795                    units: NumericSuffix::Mm,
10796                }),
10797                y: Expr::Var(Number {
10798                    value: 10.0,
10799                    units: NumericSuffix::Mm,
10800                }),
10801            },
10802            center: Point2d {
10803                x: Expr::Var(Number {
10804                    value: 10.0,
10805                    units: NumericSuffix::Mm,
10806                }),
10807                y: Expr::Var(Number {
10808                    value: 0.0,
10809                    units: NumericSuffix::Mm,
10810                }),
10811            },
10812            direction: None,
10813            construction: None,
10814        };
10815        let (_src_delta, scene_delta) = frontend
10816            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
10817            .await
10818            .unwrap();
10819        // The arc is the last object in new_objects (after the 3 points: start, end, center)
10820        let arc_id = *scene_delta.new_objects.last().unwrap();
10821
10822        // Add a line segment
10823        let line_ctor = LineCtor {
10824            start: Point2d {
10825                x: Expr::Var(Number {
10826                    value: 20.0,
10827                    units: NumericSuffix::Mm,
10828                }),
10829                y: Expr::Var(Number {
10830                    value: 0.0,
10831                    units: NumericSuffix::Mm,
10832                }),
10833            },
10834            end: Point2d {
10835                x: Expr::Var(Number {
10836                    value: 30.0,
10837                    units: NumericSuffix::Mm,
10838                }),
10839                y: Expr::Var(Number {
10840                    value: 10.0,
10841                    units: NumericSuffix::Mm,
10842                }),
10843            },
10844            construction: None,
10845        };
10846        let (_src_delta, scene_delta) = frontend
10847            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
10848            .await
10849            .unwrap();
10850        // The line is the last object in new_objects (after the 2 points: start, end)
10851        let line_id = *scene_delta.new_objects.last().unwrap();
10852
10853        // Attempt to add an invalid coincident constraint between arc and line
10854        // This should fail during execution, but state should remain intact
10855        let constraint = Constraint::Coincident(Coincident {
10856            segments: vec![arc_id.into(), line_id.into()],
10857        });
10858        let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
10859
10860        // The constraint addition should fail (invalid constraint)
10861        assert!(result.is_err(), "Expected invalid coincident constraint to fail");
10862
10863        // Verify state is not corrupted by checking that we can still access the scene graph
10864        // and that the original segments are still present with their source ranges
10865        let sketch_object_after =
10866            find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
10867        let sketch_after = expect_sketch(sketch_object_after);
10868
10869        // Verify both segments are still in the sketch
10870        assert!(
10871            sketch_after.segments.contains(&arc_id),
10872            "Arc segment should still exist after failed constraint"
10873        );
10874        assert!(
10875            sketch_after.segments.contains(&line_id),
10876            "Line segment should still exist after failed constraint"
10877        );
10878
10879        // Verify we can still access segment objects (this would fail if source ranges were corrupted)
10880        let arc_obj = frontend
10881            .scene_graph
10882            .objects
10883            .get(arc_id.0)
10884            .expect("Arc object should still be accessible");
10885        let line_obj = frontend
10886            .scene_graph
10887            .objects
10888            .get(line_id.0)
10889            .expect("Line object should still be accessible");
10890
10891        // Verify source ranges are still valid (not corrupted)
10892        // Just verify that the objects are still accessible and have the expected types
10893        match &arc_obj.kind {
10894            ObjectKind::Segment {
10895                segment: Segment::Arc(_),
10896            } => {}
10897            _ => panic!("Arc object should still be an arc segment"),
10898        }
10899        match &line_obj.kind {
10900            ObjectKind::Segment {
10901                segment: Segment::Line(_),
10902            } => {}
10903            _ => panic!("Line object should still be a line segment"),
10904        }
10905
10906        ctx.close().await;
10907        mock_ctx.close().await;
10908    }
10909
10910    #[tokio::test(flavor = "multi_thread")]
10911    async fn test_distance_two_points() {
10912        let initial_source = "\
10913sketch(on = XY) {
10914  point(at = [var 1, var 2])
10915  point(at = [var 3, var 4])
10916}
10917";
10918
10919        let program = Program::parse(initial_source).unwrap().0.unwrap();
10920
10921        let mut frontend = FrontendState::new();
10922
10923        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10924        let mock_ctx = ExecutorContext::new_mock(None).await;
10925        let version = Version(0);
10926
10927        frontend.hack_set_program(&ctx, program).await.unwrap();
10928        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10929        let sketch_id = sketch_object.id;
10930        let sketch = expect_sketch(sketch_object);
10931        let point0_id = *sketch.segments.first().unwrap();
10932        let point1_id = *sketch.segments.get(1).unwrap();
10933
10934        let constraint = Constraint::Distance(Distance {
10935            segments: vec![point0_id.into(), point1_id.into()],
10936            distance: Number {
10937                value: 2.0,
10938                units: NumericSuffix::Mm,
10939            },
10940            label_position: None,
10941            source: Default::default(),
10942        });
10943        let (src_delta, scene_delta) = frontend
10944            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10945            .await
10946            .unwrap();
10947        insta::assert_snapshot!("test_distance_two_points", src_delta.text.as_str());
10948        assert_eq!(
10949            scene_delta.new_graph.objects.len(),
10950            5,
10951            "{:#?}",
10952            scene_delta.new_graph.objects
10953        );
10954
10955        ctx.close().await;
10956        mock_ctx.close().await;
10957    }
10958
10959    #[tokio::test(flavor = "multi_thread")]
10960    async fn test_distance_two_points_with_label() {
10961        let initial_source = "\
10962sketch(on = XY) {
10963  point(at = [var 1, var 2])
10964  point(at = [var 3, var 4])
10965}
10966";
10967
10968        let program = Program::parse(initial_source).unwrap().0.unwrap();
10969
10970        let mut frontend = FrontendState::new();
10971
10972        let mock_ctx = ExecutorContext::new_mock(None).await;
10973        let version = Version(0);
10974
10975        frontend.program = program.clone();
10976        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10977        frontend.update_state_after_exec(outcome, true);
10978        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10979        let sketch_id = sketch_object.id;
10980        let sketch = expect_sketch(sketch_object);
10981        let point0_id = *sketch.segments.first().unwrap();
10982        let point1_id = *sketch.segments.get(1).unwrap();
10983
10984        let label_position = Point2d {
10985            x: Number {
10986                value: 10.0,
10987                units: NumericSuffix::Mm,
10988            },
10989            y: Number {
10990                value: 11.0,
10991                units: NumericSuffix::Mm,
10992            },
10993        };
10994        let constraint = Constraint::Distance(Distance {
10995            segments: vec![point0_id.into(), point1_id.into()],
10996            distance: Number {
10997                value: 2.0,
10998                units: NumericSuffix::Mm,
10999            },
11000            label_position: Some(label_position.clone()),
11001            source: Default::default(),
11002        });
11003        let (src_delta, scene_delta) = frontend
11004            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11005            .await
11006            .unwrap();
11007        insta::assert_snapshot!("test_distance_two_points_with_label", src_delta.text.as_str());
11008
11009        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11010        let sketch = expect_sketch(sketch_object);
11011        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11012        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11013            panic!("Expected constraint object");
11014        };
11015        let Constraint::Distance(distance) = constraint else {
11016            panic!("Expected distance constraint");
11017        };
11018        assert_eq!(distance.label_position, Some(label_position));
11019
11020        mock_ctx.close().await;
11021    }
11022
11023    #[tokio::test(flavor = "multi_thread")]
11024    async fn test_edit_distance_constraint_label_position() {
11025        let initial_source = "\
11026sketch(on = XY) {
11027  point(at = [var 1, var 2])
11028  point(at = [var 3, var 2])
11029}
11030";
11031
11032        let program = Program::parse(initial_source).unwrap().0.unwrap();
11033
11034        let mut frontend = FrontendState::new();
11035
11036        let mock_ctx = ExecutorContext::new_mock(None).await;
11037        let version = Version(0);
11038
11039        frontend.program = program.clone();
11040        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11041        frontend.update_state_after_exec(outcome, true);
11042        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11043        let sketch_id = sketch_object.id;
11044        let sketch = expect_sketch(sketch_object);
11045        let point0_id = *sketch.segments.first().unwrap();
11046        let point1_id = *sketch.segments.get(1).unwrap();
11047
11048        let constraint = Constraint::Distance(Distance {
11049            segments: vec![point0_id.into(), point1_id.into()],
11050            distance: Number {
11051                value: 2.0,
11052                units: NumericSuffix::Mm,
11053            },
11054            label_position: None,
11055            source: Default::default(),
11056        });
11057        let (_, scene_delta) = frontend
11058            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11059            .await
11060            .unwrap();
11061        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11062        let sketch = expect_sketch(sketch_object);
11063        let constraint_id = sketch.constraints[0];
11064        let label_position = Point2d {
11065            x: Number {
11066                value: 10.0,
11067                units: NumericSuffix::Mm,
11068            },
11069            y: Number {
11070                value: 11.0,
11071                units: NumericSuffix::Mm,
11072            },
11073        };
11074
11075        let (src_delta, scene_delta) = frontend
11076            .edit_distance_constraint_label_position(
11077                &mock_ctx,
11078                version,
11079                sketch_id,
11080                constraint_id,
11081                label_position.clone(),
11082                vec![],
11083            )
11084            .await
11085            .unwrap();
11086        insta::assert_snapshot!("test_edit_distance_constraint_label_position", src_delta.text.as_str());
11087
11088        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11089        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11090            panic!("Expected constraint object");
11091        };
11092        let Constraint::Distance(distance) = constraint else {
11093            panic!("Expected distance constraint");
11094        };
11095        assert_eq!(distance.label_position, Some(label_position));
11096
11097        mock_ctx.close().await;
11098    }
11099
11100    #[tokio::test(flavor = "multi_thread")]
11101    async fn test_edit_distance_constraint_type_and_value() {
11102        let initial_source = "\
11103sketch(on = XY) {
11104  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 3mm])
11105  distance([line1.start, line1.end]) == 5mm
11106}
11107";
11108
11109        let program = Program::parse(initial_source).unwrap().0.unwrap();
11110        let mut frontend = FrontendState::new();
11111        let mock_ctx = ExecutorContext::new_mock(None).await;
11112        let version = Version(0);
11113
11114        frontend.program = program.clone();
11115        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11116        frontend.update_state_after_exec(outcome, true);
11117        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11118        let sketch_id = sketch_object.id;
11119        let sketch = expect_sketch(sketch_object);
11120        let constraint_id = sketch.constraints[0];
11121        let point0_id = sketch.segments[0];
11122        let point1_id = sketch.segments[1];
11123        let label_position = Point2d {
11124            x: Number {
11125                value: 2.0,
11126                units: NumericSuffix::Mm,
11127            },
11128            y: Number {
11129                value: 5.0,
11130                units: NumericSuffix::Mm,
11131            },
11132        };
11133
11134        let (source_delta, scene_delta) = frontend
11135            .edit_distance_constraint_with_options(
11136                &mock_ctx,
11137                version,
11138                sketch_id,
11139                constraint_id,
11140                Constraint::HorizontalDistance(Distance {
11141                    segments: vec![point0_id.into(), point1_id.into()],
11142                    distance: Number {
11143                        value: 4.0,
11144                        units: NumericSuffix::Mm,
11145                    },
11146                    label_position: Some(label_position.clone()),
11147                    source: Default::default(),
11148                }),
11149                EditConstraintOptions {
11150                    commit_solved_initial_guesses: false,
11151                },
11152            )
11153            .await
11154            .unwrap();
11155        assert_eq!(
11156            source_delta.text,
11157            "\
11158sketch(on = XY) {
11159  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 3mm])
11160  horizontalDistance([line1.start, line1.end], labelPosition = [2mm, 5mm]) == 4mm
11161}
11162"
11163        );
11164
11165        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11166        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11167            panic!("Expected constraint object");
11168        };
11169        let Constraint::HorizontalDistance(distance) = constraint else {
11170            panic!("Expected horizontal distance constraint");
11171        };
11172        assert_eq!(distance.distance.value, 4.0);
11173        assert_eq!(distance.label_position, Some(label_position));
11174
11175        mock_ctx.close().await;
11176    }
11177
11178    #[tokio::test(flavor = "multi_thread")]
11179    async fn test_edit_angle_constraint_label_position() {
11180        let initial_source = "\
11181sketch(on = XY) {
11182  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11183  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11184  angle([line1, line2]) == 60deg
11185}
11186";
11187
11188        let program = Program::parse(initial_source).unwrap().0.unwrap();
11189        let mut frontend = FrontendState::new();
11190        let mock_ctx = ExecutorContext::new_mock(None).await;
11191        let version = Version(0);
11192
11193        frontend.program = program.clone();
11194        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11195        frontend.update_state_after_exec(outcome, true);
11196        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11197        let sketch_id = sketch_object.id;
11198        let sketch = expect_sketch(sketch_object);
11199        let constraint_id = sketch.constraints[0];
11200        let label_position = Point2d {
11201            x: Number {
11202                value: 10.0,
11203                units: NumericSuffix::Mm,
11204            },
11205            y: Number {
11206                value: 11.0,
11207                units: NumericSuffix::Mm,
11208            },
11209        };
11210
11211        let (src_delta, scene_delta) = frontend
11212            .edit_distance_constraint_label_position(
11213                &mock_ctx,
11214                version,
11215                sketch_id,
11216                constraint_id,
11217                label_position.clone(),
11218                vec![],
11219            )
11220            .await
11221            .unwrap();
11222        assert_eq!(
11223            src_delta.text.as_str(),
11224            "\
11225sketch(on = XY) {
11226  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11227  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.46mm])
11228  angle([line1, line2], labelPosition = [10mm, 11mm]) == 60deg
11229}
11230"
11231        );
11232
11233        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11234        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11235            panic!("Expected constraint object");
11236        };
11237        let Constraint::Angle(angle) = constraint else {
11238            panic!("Expected angle constraint");
11239        };
11240        assert_eq!(angle.label_position, Some(label_position));
11241
11242        mock_ctx.close().await;
11243    }
11244
11245    #[tokio::test(flavor = "multi_thread")]
11246    async fn test_edit_angle_constraint_label_position_with_call_on_right() {
11247        let initial_source = "\
11248sketch(on = XY) {
11249  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11250  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11251  60deg == angleDimension(lines = [line1, line2], sector = 1)
11252}
11253";
11254
11255        let program = Program::parse(initial_source).unwrap().0.unwrap();
11256        let mut frontend = FrontendState::new();
11257        let mock_ctx = ExecutorContext::new_mock(None).await;
11258        let version = Version(0);
11259
11260        frontend.program = program.clone();
11261        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11262        frontend.update_state_after_exec(outcome, true);
11263        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11264        let sketch_id = sketch_object.id;
11265        let sketch = expect_sketch(sketch_object);
11266        let constraint_id = sketch.constraints[0];
11267        let label_position = Point2d {
11268            x: Number {
11269                value: 10.0,
11270                units: NumericSuffix::Mm,
11271            },
11272            y: Number {
11273                value: 11.0,
11274                units: NumericSuffix::Mm,
11275            },
11276        };
11277
11278        let (src_delta, scene_delta) = frontend
11279            .edit_distance_constraint_label_position(
11280                &mock_ctx,
11281                version,
11282                sketch_id,
11283                constraint_id,
11284                label_position.clone(),
11285                vec![],
11286            )
11287            .await
11288            .unwrap();
11289        assert_eq!(
11290            src_delta.text.as_str(),
11291            "\
11292sketch(on = XY) {
11293  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11294  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.46mm])
11295  60deg == angleDimension(lines = [line1, line2], sector = 1, labelPosition = [10mm, 11mm])
11296}
11297"
11298        );
11299
11300        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11301        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11302            panic!("Expected constraint object");
11303        };
11304        let Constraint::Angle(angle) = constraint else {
11305            panic!("Expected angle constraint");
11306        };
11307        assert_eq!(angle.label_position, Some(label_position));
11308
11309        mock_ctx.close().await;
11310    }
11311
11312    #[tokio::test(flavor = "multi_thread")]
11313    async fn test_edit_angle_constraint() {
11314        let initial_source = "\
11315sketch(on = XY) {
11316  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11317  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11318  angle([line1, line2]) == 60deg
11319}
11320";
11321
11322        let program = Program::parse(initial_source).unwrap().0.unwrap();
11323        let mut frontend = FrontendState::new();
11324        let mock_ctx = ExecutorContext::new_mock(None).await;
11325        let version = Version(0);
11326
11327        frontend.program = program.clone();
11328        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11329        frontend.update_state_after_exec(outcome, true);
11330        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11331        let sketch_id = sketch_object.id;
11332        let sketch = expect_sketch(sketch_object);
11333        let constraint_id = sketch.constraints[0];
11334        let line1_id = *sketch.segments.get(2).unwrap();
11335        let line2_id = *sketch.segments.get(5).unwrap();
11336        let label_position = Point2d {
11337            x: Number {
11338                value: 10.0,
11339                units: NumericSuffix::Mm,
11340            },
11341            y: Number {
11342                value: 11.0,
11343                units: NumericSuffix::Mm,
11344            },
11345        };
11346
11347        let (src_delta, scene_delta) = frontend
11348            .edit_angle_constraint_with_options(
11349                &mock_ctx,
11350                version,
11351                sketch_id,
11352                constraint_id,
11353                Angle {
11354                    lines: vec![line2_id, line1_id],
11355                    angle: Number {
11356                        value: 60.0,
11357                        units: NumericSuffix::Deg,
11358                    },
11359                    sector: Some(3),
11360                    inverse: Some(false),
11361                    label_position: Some(label_position.clone()),
11362                    source: Default::default(),
11363                },
11364                EditConstraintOptions {
11365                    commit_solved_initial_guesses: false,
11366                },
11367            )
11368            .await
11369            .unwrap();
11370        assert_eq!(
11371            src_delta.text.as_str(),
11372            "\
11373sketch(on = XY) {
11374  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11375  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11376  angleDimension(lines = [line2, line1], sector = 3, labelPosition = [10mm, 11mm]) == 60deg
11377}
11378"
11379        );
11380
11381        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11382        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11383            panic!("Expected constraint object");
11384        };
11385        let Constraint::Angle(angle) = constraint else {
11386            panic!("Expected angle constraint");
11387        };
11388        assert_eq!(angle.lines, vec![line2_id, line1_id]);
11389        assert_eq!(angle.sector, Some(3));
11390        assert_eq!(angle.inverse, Some(false));
11391        assert_eq!(angle.label_position, Some(label_position));
11392
11393        mock_ctx.close().await;
11394    }
11395
11396    #[tokio::test(flavor = "multi_thread")]
11397    async fn test_edit_angle_constraint_with_call_on_right() {
11398        let initial_source = "\
11399sketch(on = XY) {
11400  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11401  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11402  60deg == angle([line1, line2])
11403}
11404";
11405
11406        let program = Program::parse(initial_source).unwrap().0.unwrap();
11407        let mut frontend = FrontendState::new();
11408        let mock_ctx = ExecutorContext::new_mock(None).await;
11409        let version = Version(0);
11410
11411        frontend.program = program.clone();
11412        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11413        frontend.update_state_after_exec(outcome, true);
11414        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11415        let sketch_id = sketch_object.id;
11416        let sketch = expect_sketch(sketch_object);
11417        let constraint_id = sketch.constraints[0];
11418        let line1_id = *sketch.segments.get(2).unwrap();
11419        let line2_id = *sketch.segments.get(5).unwrap();
11420
11421        let (src_delta, _) = frontend
11422            .edit_angle_constraint_with_options(
11423                &mock_ctx,
11424                version,
11425                sketch_id,
11426                constraint_id,
11427                Angle {
11428                    lines: vec![line2_id, line1_id],
11429                    angle: Number {
11430                        value: 60.0,
11431                        units: NumericSuffix::Deg,
11432                    },
11433                    sector: Some(3),
11434                    inverse: Some(false),
11435                    label_position: None,
11436                    source: Default::default(),
11437                },
11438                EditConstraintOptions {
11439                    commit_solved_initial_guesses: false,
11440                },
11441            )
11442            .await
11443            .unwrap();
11444        assert_eq!(
11445            src_delta.text.as_str(),
11446            "\
11447sketch(on = XY) {
11448  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11449  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11450  60deg == angleDimension(lines = [line2, line1], sector = 3)
11451}
11452"
11453        );
11454
11455        mock_ctx.close().await;
11456    }
11457
11458    #[tokio::test(flavor = "multi_thread")]
11459    async fn test_edit_segments_can_commit_constraint_label_position_in_same_execution() {
11460        let initial_source = "\
11461@settings(kclVersion = 2.0)
11462
11463sketch001 = sketch(on = XZ) {
11464  line1 = line(start = [var 0mm, var 12.55mm], end = [var -6.03mm, var 8.51mm])
11465  line3 = line(start = [var -7.41mm, var 2.92mm], end = [var -1.47mm, var 4.32mm])
11466  distance([line1.start, line3.end], labelPosition = [5.56mm, 8.65mm]) == 8.36mm
11467  vertical([line1.start, ORIGIN])
11468}
11469";
11470
11471        let program = Program::parse(initial_source).unwrap().0.unwrap();
11472        let mut frontend = FrontendState::new();
11473        let mock_ctx = ExecutorContext::new_mock(None).await;
11474        let version = Version(0);
11475
11476        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
11477        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11478        let sketch_id = sketch_object.id;
11479        let sketch = expect_sketch(sketch_object);
11480        let constraint_id = sketch
11481            .constraints
11482            .iter()
11483            .copied()
11484            .find(|constraint_id| {
11485                matches!(
11486                    frontend.scene_graph.objects[constraint_id.0].kind,
11487                    ObjectKind::Constraint {
11488                        constraint: Constraint::Distance(_)
11489                    }
11490                )
11491            })
11492            .unwrap();
11493        let line1_id = sketch
11494            .segments
11495            .iter()
11496            .copied()
11497            .find(|segment_id| {
11498                matches!(
11499                    frontend.scene_graph.objects[segment_id.0].kind,
11500                    ObjectKind::Segment {
11501                        segment: Segment::Line(_)
11502                    }
11503                )
11504            })
11505            .unwrap();
11506        let label_position = Point2d {
11507            x: Number {
11508                value: 7.0,
11509                units: NumericSuffix::Mm,
11510            },
11511            y: Number {
11512                value: 9.0,
11513                units: NumericSuffix::Mm,
11514            },
11515        };
11516
11517        let (source_delta, scene_delta) = frontend
11518            .edit_segments_with_options(
11519                &mock_ctx,
11520                version,
11521                sketch_id,
11522                vec![ExistingSegmentCtor {
11523                    id: line1_id,
11524                    ctor: SegmentCtor::Line(LineCtor {
11525                        start: point_expr_mm(2.0, 15.55),
11526                        end: point_expr_mm(-4.03, 11.51),
11527                        construction: None,
11528                    }),
11529                }],
11530                EditSegmentsOptions {
11531                    anchor_segment_ids: Some(vec![]),
11532                    drag_anchors: vec![SegmentDragAnchor {
11533                        segment_id: line1_id,
11534                        target: label_position.clone(),
11535                    }],
11536                    constraint_label_edits: vec![ConstraintLabelPositionEdit {
11537                        constraint_id,
11538                        label_position: label_position.clone(),
11539                    }],
11540                    commit_solved_initial_guesses: true,
11541                },
11542            )
11543            .await
11544            .unwrap();
11545
11546        assert!(source_delta.text.contains("labelPosition = [7mm, 9mm]"));
11547        let constraint_object = &scene_delta.new_graph.objects[constraint_id.0];
11548        let ObjectKind::Constraint {
11549            constraint: Constraint::Distance(distance),
11550        } = &constraint_object.kind
11551        else {
11552            panic!("Expected distance constraint object");
11553        };
11554        assert_eq!(distance.label_position, Some(label_position));
11555
11556        let snapped_label_position = Point2d {
11557            x: Number {
11558                value: 8.0,
11559                units: NumericSuffix::Mm,
11560            },
11561            y: Number {
11562                value: 10.0,
11563                units: NumericSuffix::Mm,
11564            },
11565        };
11566        let (source_delta, scene_delta) = frontend
11567            .edit_segments_with_options(
11568                &mock_ctx,
11569                version,
11570                sketch_id,
11571                vec![],
11572                EditSegmentsOptions {
11573                    anchor_segment_ids: Some(vec![line1_id]),
11574                    drag_anchors: vec![],
11575                    constraint_label_edits: vec![ConstraintLabelPositionEdit {
11576                        constraint_id,
11577                        label_position: snapped_label_position.clone(),
11578                    }],
11579                    commit_solved_initial_guesses: true,
11580                },
11581            )
11582            .await
11583            .unwrap();
11584
11585        assert!(source_delta.text.contains("labelPosition = [8mm, 10mm]"));
11586        let constraint_object = &scene_delta.new_graph.objects[constraint_id.0];
11587        let ObjectKind::Constraint {
11588            constraint: Constraint::Distance(distance),
11589        } = &constraint_object.kind
11590        else {
11591            panic!("Expected distance constraint object");
11592        };
11593        assert_eq!(distance.label_position, Some(snapped_label_position));
11594
11595        mock_ctx.close().await;
11596    }
11597
11598    #[tokio::test(flavor = "multi_thread")]
11599    async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
11600        let initial_source = "\
11601sketch(on = XY) {
11602  point1 = point(at = [var 0mm, var 0mm])
11603  point2 = point(at = [var 10mm, var 0mm])
11604  distance([point1, point2]) == 5mm
11605}
11606";
11607
11608        let program = Program::parse(initial_source).unwrap().0.unwrap();
11609        let mut frontend = FrontendState::new();
11610        let mock_ctx = ExecutorContext::new_mock(None).await;
11611        let version = Version(0);
11612
11613        frontend.program = program.clone();
11614        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11615        frontend.update_state_after_exec(outcome, true);
11616        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11617        let sketch_id = sketch_object.id;
11618        let sketch = expect_sketch(sketch_object);
11619        let point0_id = sketch.segments[0];
11620        let point1_id = sketch.segments[1];
11621        let constraint_id = sketch.constraints[0];
11622
11623        let edited_segments = vec![ExistingSegmentCtor {
11624            id: point0_id,
11625            ctor: SegmentCtor::Point(PointCtor {
11626                position: Point2d {
11627                    x: Expr::Var(Number {
11628                        value: 2.0,
11629                        units: NumericSuffix::Mm,
11630                    }),
11631                    y: Expr::Var(Number {
11632                        value: 1.0,
11633                        units: NumericSuffix::Mm,
11634                    }),
11635                },
11636            }),
11637        }];
11638        let (_, scene_delta) = frontend
11639            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
11640            .await
11641            .unwrap();
11642        let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
11643        let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
11644
11645        let label_position = Point2d {
11646            x: Number {
11647                value: 3.0,
11648                units: NumericSuffix::Mm,
11649            },
11650            y: Number {
11651                value: 4.0,
11652                units: NumericSuffix::Mm,
11653            },
11654        };
11655        let (_, scene_delta) = frontend
11656            .edit_distance_constraint_label_position(
11657                &mock_ctx,
11658                version,
11659                sketch_id,
11660                constraint_id,
11661                label_position,
11662                vec![point0_id],
11663            )
11664            .await
11665            .unwrap();
11666
11667        assert_point_position_close(
11668            point_position(&scene_delta.new_graph, point0_id),
11669            point0_after_segment_edit,
11670        );
11671        assert_point_position_close(
11672            point_position(&scene_delta.new_graph, point1_id),
11673            point1_after_segment_edit,
11674        );
11675
11676        mock_ctx.close().await;
11677    }
11678
11679    #[tokio::test(flavor = "multi_thread")]
11680    async fn test_distance_point_line() {
11681        let initial_source = "\
11682sketch(on = XY) {
11683  point(at = [var 0, var 5])
11684  line(start = [var 0, var 0], end = [var 10, var 0])
11685}
11686";
11687
11688        let program = Program::parse(initial_source).unwrap().0.unwrap();
11689
11690        let mut frontend = FrontendState::new();
11691
11692        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11693        let mock_ctx = ExecutorContext::new_mock(None).await;
11694        let version = Version(0);
11695
11696        frontend.hack_set_program(&ctx, program).await.unwrap();
11697        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11698        let sketch_id = sketch_object.id;
11699        let sketch = expect_sketch(sketch_object);
11700        let point_id = *sketch.segments.first().unwrap();
11701        let line_id = *sketch
11702            .segments
11703            .iter()
11704            .find(|segment_id| {
11705                matches!(
11706                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11707                    Some(ObjectKind::Segment {
11708                        segment: Segment::Line(_)
11709                    })
11710                )
11711            })
11712            .unwrap();
11713
11714        let label_position = Point2d {
11715            x: Number {
11716                value: 10.0,
11717                units: NumericSuffix::Mm,
11718            },
11719            y: Number {
11720                value: 11.0,
11721                units: NumericSuffix::Mm,
11722            },
11723        };
11724        let constraint = Constraint::Distance(Distance {
11725            segments: vec![point_id.into(), line_id.into()],
11726            distance: Number {
11727                value: 5.0,
11728                units: NumericSuffix::Mm,
11729            },
11730            label_position: Some(label_position.clone()),
11731            source: Default::default(),
11732        });
11733        let (src_delta, scene_delta) = frontend
11734            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11735            .await
11736            .unwrap();
11737        insta::assert_snapshot!("test_distance_point_line", src_delta.text.as_str());
11738        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11739        let sketch = expect_sketch(sketch_object);
11740        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11741        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11742            panic!("Expected constraint object");
11743        };
11744        let Constraint::Distance(distance) = constraint else {
11745            panic!("Expected distance constraint");
11746        };
11747        assert_eq!(distance.label_position, Some(label_position));
11748
11749        ctx.close().await;
11750        mock_ctx.close().await;
11751    }
11752
11753    #[tokio::test(flavor = "multi_thread")]
11754    async fn test_distance_point_arc() {
11755        let initial_source = "\
11756sketch(on = XY) {
11757  point(at = [var 0, var 8])
11758  arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
11759}
11760";
11761
11762        let program = Program::parse(initial_source).unwrap().0.unwrap();
11763
11764        let mut frontend = FrontendState::new();
11765
11766        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11767        let mock_ctx = ExecutorContext::new_mock(None).await;
11768        let version = Version(0);
11769
11770        frontend.hack_set_program(&ctx, program).await.unwrap();
11771        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11772        let sketch_id = sketch_object.id;
11773        let sketch = expect_sketch(sketch_object);
11774        let point_id = *sketch.segments.first().unwrap();
11775        let arc_id = *sketch
11776            .segments
11777            .iter()
11778            .find(|segment_id| {
11779                matches!(
11780                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11781                    Some(ObjectKind::Segment {
11782                        segment: Segment::Arc(_)
11783                    })
11784                )
11785            })
11786            .unwrap();
11787
11788        let constraint = Constraint::Distance(Distance {
11789            segments: vec![point_id.into(), arc_id.into()],
11790            distance: Number {
11791                value: 3.0,
11792                units: NumericSuffix::Mm,
11793            },
11794            label_position: None,
11795            source: Default::default(),
11796        });
11797        let (src_delta, _scene_delta) = frontend
11798            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11799            .await
11800            .unwrap();
11801        insta::assert_snapshot!("test_distance_point_arc", src_delta.text.as_str());
11802
11803        ctx.close().await;
11804        mock_ctx.close().await;
11805    }
11806
11807    #[tokio::test(flavor = "multi_thread")]
11808    async fn test_distance_arc_origin() {
11809        let initial_source = "\
11810sketch001 = sketch(on = XY) {
11811  arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
11812}
11813";
11814
11815        let program = Program::parse(initial_source).unwrap().0.unwrap();
11816
11817        let mut frontend = FrontendState::new();
11818
11819        let mock_ctx = ExecutorContext::new_mock(None).await;
11820        let version = Version(0);
11821
11822        frontend.program = program.clone();
11823        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11824        frontend.update_state_after_exec(outcome, true);
11825        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11826        let sketch_id = sketch_object.id;
11827        let sketch = expect_sketch(sketch_object);
11828        let arc_id = *sketch
11829            .segments
11830            .iter()
11831            .find(|segment_id| {
11832                matches!(
11833                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11834                    Some(ObjectKind::Segment {
11835                        segment: Segment::Arc(_)
11836                    })
11837                )
11838            })
11839            .unwrap();
11840
11841        let constraint = Constraint::Distance(Distance {
11842            segments: vec![arc_id.into(), ConstraintSegment::ORIGIN],
11843            distance: Number {
11844                value: 3.0,
11845                units: NumericSuffix::Mm,
11846            },
11847            label_position: None,
11848            source: Default::default(),
11849        });
11850        let (src_delta, _scene_delta) = frontend
11851            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11852            .await
11853            .unwrap();
11854        insta::assert_snapshot!("test_distance_arc_origin", src_delta.text.as_str());
11855
11856        mock_ctx.close().await;
11857    }
11858
11859    #[tokio::test(flavor = "multi_thread")]
11860    async fn test_distance_line_origin() {
11861        let initial_source = "\
11862sketch(on = XY) {
11863  line(start = [var 5, var 0], end = [var 5, var 10])
11864}
11865";
11866
11867        let program = Program::parse(initial_source).unwrap().0.unwrap();
11868
11869        let mut frontend = FrontendState::new();
11870
11871        let mock_ctx = ExecutorContext::new_mock(None).await;
11872        let version = Version(0);
11873
11874        frontend.program = program.clone();
11875        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11876        frontend.update_state_after_exec(outcome, true);
11877        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11878        let sketch_id = sketch_object.id;
11879        let sketch = expect_sketch(sketch_object);
11880        let line_id = *sketch
11881            .segments
11882            .iter()
11883            .find(|segment_id| {
11884                matches!(
11885                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11886                    Some(ObjectKind::Segment {
11887                        segment: Segment::Line(_)
11888                    })
11889                )
11890            })
11891            .unwrap();
11892
11893        let constraint = Constraint::Distance(Distance {
11894            segments: vec![ConstraintSegment::ORIGIN, line_id.into()],
11895            distance: Number {
11896                value: 5.0,
11897                units: NumericSuffix::Mm,
11898            },
11899            label_position: None,
11900            source: Default::default(),
11901        });
11902        let (src_delta, _scene_delta) = frontend
11903            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11904            .await
11905            .unwrap();
11906        insta::assert_snapshot!("test_distance_line_origin", src_delta.text.as_str());
11907
11908        mock_ctx.close().await;
11909    }
11910
11911    #[tokio::test(flavor = "multi_thread")]
11912    async fn test_distance_line_circle() {
11913        let initial_source = "\
11914sketch(on = XY) {
11915  line(start = [var -10, var 8], end = [var 10, var 8])
11916  circle(start = [var 5, var 0], center = [var 0, var 0])
11917}
11918";
11919
11920        let program = Program::parse(initial_source).unwrap().0.unwrap();
11921
11922        let mut frontend = FrontendState::new();
11923
11924        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11925        let mock_ctx = ExecutorContext::new_mock(None).await;
11926        let version = Version(0);
11927
11928        frontend.hack_set_program(&ctx, program).await.unwrap();
11929        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11930        let sketch_id = sketch_object.id;
11931        let sketch = expect_sketch(sketch_object);
11932        let line_id = *sketch
11933            .segments
11934            .iter()
11935            .find(|segment_id| {
11936                matches!(
11937                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11938                    Some(ObjectKind::Segment {
11939                        segment: Segment::Line(_)
11940                    })
11941                )
11942            })
11943            .unwrap();
11944        let circle_id = *sketch
11945            .segments
11946            .iter()
11947            .find(|segment_id| {
11948                matches!(
11949                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11950                    Some(ObjectKind::Segment {
11951                        segment: Segment::Circle(_)
11952                    })
11953                )
11954            })
11955            .unwrap();
11956
11957        let constraint = Constraint::Distance(Distance {
11958            segments: vec![line_id.into(), circle_id.into()],
11959            distance: Number {
11960                value: 3.0,
11961                units: NumericSuffix::Mm,
11962            },
11963            label_position: None,
11964            source: Default::default(),
11965        });
11966        let (src_delta, _scene_delta) = frontend
11967            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11968            .await
11969            .unwrap();
11970        insta::assert_snapshot!("test_distance_line_circle", src_delta.text.as_str());
11971
11972        ctx.close().await;
11973        mock_ctx.close().await;
11974    }
11975
11976    #[tokio::test(flavor = "multi_thread")]
11977    async fn test_distance_circle_arc() {
11978        let initial_source = "\
11979sketch(on = XY) {
11980  circle(start = [var 5, var 0], center = [var 0, var 0])
11981  arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
11982}
11983";
11984
11985        let program = Program::parse(initial_source).unwrap().0.unwrap();
11986
11987        let mut frontend = FrontendState::new();
11988
11989        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11990        let mock_ctx = ExecutorContext::new_mock(None).await;
11991        let version = Version(0);
11992
11993        frontend.hack_set_program(&ctx, program).await.unwrap();
11994        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11995        let sketch_id = sketch_object.id;
11996        let sketch = expect_sketch(sketch_object);
11997        let circle_id = *sketch
11998            .segments
11999            .iter()
12000            .find(|segment_id| {
12001                matches!(
12002                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12003                    Some(ObjectKind::Segment {
12004                        segment: Segment::Circle(_)
12005                    })
12006                )
12007            })
12008            .unwrap();
12009        let arc_id = *sketch
12010            .segments
12011            .iter()
12012            .find(|segment_id| {
12013                matches!(
12014                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12015                    Some(ObjectKind::Segment {
12016                        segment: Segment::Arc(_)
12017                    })
12018                )
12019            })
12020            .unwrap();
12021
12022        let constraint = Constraint::Distance(Distance {
12023            segments: vec![circle_id.into(), arc_id.into()],
12024            distance: Number {
12025                value: 3.0,
12026                units: NumericSuffix::Mm,
12027            },
12028            label_position: None,
12029            source: Default::default(),
12030        });
12031        let (src_delta, _scene_delta) = frontend
12032            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12033            .await
12034            .unwrap();
12035        insta::assert_snapshot!("test_distance_circle_arc", src_delta.text.as_str());
12036
12037        ctx.close().await;
12038        mock_ctx.close().await;
12039    }
12040
12041    #[tokio::test(flavor = "multi_thread")]
12042    async fn test_distance_parallel_lines() {
12043        let initial_source = "\
12044sketch(on = XY) {
12045  line(start = [var 0, var 0], end = [var 10, var 0])
12046  line(start = [var 0, var 5], end = [var 10, var 5])
12047}
12048";
12049
12050        let program = Program::parse(initial_source).unwrap().0.unwrap();
12051
12052        let mut frontend = FrontendState::new();
12053
12054        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12055        let mock_ctx = ExecutorContext::new_mock(None).await;
12056        let version = Version(0);
12057
12058        frontend.hack_set_program(&ctx, program).await.unwrap();
12059        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12060        let sketch_id = sketch_object.id;
12061        let sketch = expect_sketch(sketch_object);
12062        let line_ids = sketch
12063            .segments
12064            .iter()
12065            .copied()
12066            .filter(|segment_id| {
12067                matches!(
12068                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12069                    Some(ObjectKind::Segment {
12070                        segment: Segment::Line(_)
12071                    })
12072                )
12073            })
12074            .collect::<Vec<_>>();
12075
12076        let constraint = Constraint::Distance(Distance {
12077            segments: vec![line_ids[0].into(), line_ids[1].into()],
12078            distance: Number {
12079                value: 5.0,
12080                units: NumericSuffix::Mm,
12081            },
12082            label_position: None,
12083            source: Default::default(),
12084        });
12085        let (src_delta, _scene_delta) = frontend
12086            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12087            .await
12088            .unwrap();
12089        insta::assert_snapshot!("test_distance_parallel_lines", src_delta.text.as_str());
12090
12091        ctx.close().await;
12092        mock_ctx.close().await;
12093    }
12094
12095    #[tokio::test(flavor = "multi_thread")]
12096    async fn test_distance_non_parallel_lines_lowers_to_distance() {
12097        // NOTE: Current LinesAtAngle constraint collapses if lines are initialized perpendicular to
12098        // one another because the gradient of the residual has no tangential component in this
12099        // configuration. The only path to reducing the residual is shrinking the lengths of the
12100        // lines which are causing the lines to collapse, producing a degenerate output.
12101        let initial_source = "\
12102sketch(on = XY) {
12103  line(start = [var 0, var 0], end = [var 10, var 0])
12104  line(start = [var 0, var 0], end = [var 10, var 10])
12105}
12106";
12107
12108        let program = Program::parse(initial_source).unwrap().0.unwrap();
12109
12110        let mut frontend = FrontendState::new();
12111
12112        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12113        let mock_ctx = ExecutorContext::new_mock(None).await;
12114        let version = Version(0);
12115
12116        frontend.hack_set_program(&ctx, program).await.unwrap();
12117        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12118        let sketch_id = sketch_object.id;
12119        let sketch = expect_sketch(sketch_object);
12120        let line_ids = sketch
12121            .segments
12122            .iter()
12123            .copied()
12124            .filter(|segment_id| {
12125                matches!(
12126                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12127                    Some(ObjectKind::Segment {
12128                        segment: Segment::Line(_)
12129                    })
12130                )
12131            })
12132            .collect::<Vec<_>>();
12133
12134        let constraint = Constraint::Distance(Distance {
12135            segments: vec![line_ids[0].into(), line_ids[1].into()],
12136            distance: Number {
12137                value: 5.0,
12138                units: NumericSuffix::Mm,
12139            },
12140            label_position: None,
12141            source: Default::default(),
12142        });
12143        let (src_delta, _scene_delta) = frontend
12144            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12145            .await
12146            .unwrap();
12147        insta::assert_snapshot!(
12148            "test_distance_non_parallel_lines_lowers_to_distance",
12149            src_delta.text.as_str()
12150        );
12151
12152        ctx.close().await;
12153        mock_ctx.close().await;
12154    }
12155
12156    #[tokio::test(flavor = "multi_thread")]
12157    async fn test_horizontal_distance_two_points() {
12158        let initial_source = "\
12159sketch(on = XY) {
12160  point(at = [var 1, var 2])
12161  point(at = [var 3, var 4])
12162}
12163";
12164
12165        let program = Program::parse(initial_source).unwrap().0.unwrap();
12166
12167        let mut frontend = FrontendState::new();
12168
12169        let mock_ctx = ExecutorContext::new_mock(None).await;
12170        let version = Version(0);
12171
12172        frontend.program = program.clone();
12173        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12174        frontend.update_state_after_exec(outcome, true);
12175        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12176        let sketch_id = sketch_object.id;
12177        let sketch = expect_sketch(sketch_object);
12178        let point0_id = *sketch.segments.first().unwrap();
12179        let point1_id = *sketch.segments.get(1).unwrap();
12180        let label_position = Point2d {
12181            x: Number {
12182                value: 10.0,
12183                units: NumericSuffix::Mm,
12184            },
12185            y: Number {
12186                value: 11.0,
12187                units: NumericSuffix::Mm,
12188            },
12189        };
12190
12191        let constraint = Constraint::HorizontalDistance(Distance {
12192            segments: vec![point0_id.into(), point1_id.into()],
12193            distance: Number {
12194                value: 2.0,
12195                units: NumericSuffix::Mm,
12196            },
12197            label_position: Some(label_position.clone()),
12198            source: Default::default(),
12199        });
12200        let (src_delta, scene_delta) = frontend
12201            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12202            .await
12203            .unwrap();
12204        insta::assert_snapshot!("test_horizontal_distance_two_points", src_delta.text.as_str());
12205        assert_eq!(
12206            scene_delta.new_graph.objects.len(),
12207            5,
12208            "{:#?}",
12209            scene_delta.new_graph.objects
12210        );
12211        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12212        let sketch = expect_sketch(sketch_object);
12213        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12214        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12215            panic!("Expected constraint object");
12216        };
12217        let Constraint::HorizontalDistance(distance) = constraint else {
12218            panic!("Expected horizontal distance constraint");
12219        };
12220        assert_eq!(distance.label_position, Some(label_position));
12221
12222        mock_ctx.close().await;
12223    }
12224
12225    #[tokio::test(flavor = "multi_thread")]
12226    async fn test_radius_single_arc_segment() {
12227        let initial_source = "\
12228sketch(on = XY) {
12229  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12230}
12231";
12232
12233        let program = Program::parse(initial_source).unwrap().0.unwrap();
12234
12235        let mut frontend = FrontendState::new();
12236
12237        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12238        let mock_ctx = ExecutorContext::new_mock(None).await;
12239        let version = Version(0);
12240
12241        frontend.hack_set_program(&ctx, program).await.unwrap();
12242        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12243        let sketch_id = sketch_object.id;
12244        let sketch = expect_sketch(sketch_object);
12245        // Find the arc segment (not the points)
12246        let arc_id = sketch
12247            .segments
12248            .iter()
12249            .find(|&seg_id| {
12250                let obj = frontend.scene_graph.objects.get(seg_id.0);
12251                matches!(
12252                    obj.map(|o| &o.kind),
12253                    Some(ObjectKind::Segment {
12254                        segment: Segment::Arc(_)
12255                    })
12256                )
12257            })
12258            .unwrap();
12259
12260        let constraint = Constraint::Radius(Radius {
12261            arc: *arc_id,
12262            radius: Number {
12263                value: 5.0,
12264                units: NumericSuffix::Mm,
12265            },
12266            label_position: None,
12267            source: Default::default(),
12268        });
12269        let (src_delta, scene_delta) = frontend
12270            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12271            .await
12272            .unwrap();
12273        insta::assert_snapshot!("test_radius_single_arc_segment", src_delta.text.as_str());
12274        assert_eq!(
12275            scene_delta.new_graph.objects.len(),
12276            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
12277            "{:#?}",
12278            scene_delta.new_graph.objects
12279        );
12280
12281        ctx.close().await;
12282        mock_ctx.close().await;
12283    }
12284
12285    #[tokio::test(flavor = "multi_thread")]
12286    async fn test_radius_single_arc_segment_with_label_position() {
12287        let initial_source = "\
12288sketch(on = XY) {
12289  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12290}
12291";
12292
12293        let program = Program::parse(initial_source).unwrap().0.unwrap();
12294        let mut frontend = FrontendState::new();
12295        let mock_ctx = ExecutorContext::new_mock(None).await;
12296        let version = Version(0);
12297
12298        frontend.program = program.clone();
12299        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12300        frontend.update_state_after_exec(outcome, true);
12301        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12302        let sketch_id = sketch_object.id;
12303        let sketch = expect_sketch(sketch_object);
12304        let arc_id = sketch
12305            .segments
12306            .iter()
12307            .find(|&seg_id| {
12308                let obj = frontend.scene_graph.objects.get(seg_id.0);
12309                matches!(
12310                    obj.map(|o| &o.kind),
12311                    Some(ObjectKind::Segment {
12312                        segment: Segment::Arc(_)
12313                    })
12314                )
12315            })
12316            .unwrap();
12317
12318        let label_position = Point2d {
12319            x: Number {
12320                value: 10.0,
12321                units: NumericSuffix::Mm,
12322            },
12323            y: Number {
12324                value: 11.0,
12325                units: NumericSuffix::Mm,
12326            },
12327        };
12328        let constraint = Constraint::Radius(Radius {
12329            arc: *arc_id,
12330            radius: Number {
12331                value: 5.0,
12332                units: NumericSuffix::Mm,
12333            },
12334            label_position: Some(label_position.clone()),
12335            source: Default::default(),
12336        });
12337        let (src_delta, scene_delta) = frontend
12338            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12339            .await
12340            .unwrap();
12341        insta::assert_snapshot!(
12342            "test_radius_single_arc_segment_with_label_position",
12343            src_delta.text.as_str()
12344        );
12345
12346        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12347        let sketch = expect_sketch(sketch_object);
12348        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12349        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12350            panic!("Expected constraint object");
12351        };
12352        let Constraint::Radius(radius) = constraint else {
12353            panic!("Expected radius constraint");
12354        };
12355        assert_eq!(radius.label_position, Some(label_position));
12356
12357        mock_ctx.close().await;
12358    }
12359
12360    #[tokio::test(flavor = "multi_thread")]
12361    async fn test_edit_radius_constraint_label_position() {
12362        let initial_source = "\
12363sketch(on = XY) {
12364  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12365  radius(arc1) == 5mm
12366}
12367";
12368
12369        let program = Program::parse(initial_source).unwrap().0.unwrap();
12370        let mut frontend = FrontendState::new();
12371        let mock_ctx = ExecutorContext::new_mock(None).await;
12372        let version = Version(0);
12373
12374        frontend.program = program.clone();
12375        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12376        frontend.update_state_after_exec(outcome, true);
12377        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12378        let sketch_id = sketch_object.id;
12379        let sketch = expect_sketch(sketch_object);
12380        let constraint_id = sketch.constraints[0];
12381        let label_position = Point2d {
12382            x: Number {
12383                value: 10.0,
12384                units: NumericSuffix::Mm,
12385            },
12386            y: Number {
12387                value: 11.0,
12388                units: NumericSuffix::Mm,
12389            },
12390        };
12391
12392        let (src_delta, scene_delta) = frontend
12393            .edit_distance_constraint_label_position(
12394                &mock_ctx,
12395                version,
12396                sketch_id,
12397                constraint_id,
12398                label_position.clone(),
12399                vec![],
12400            )
12401            .await
12402            .unwrap();
12403        insta::assert_snapshot!("test_edit_radius_constraint_label_position", src_delta.text.as_str());
12404
12405        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12406        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12407            panic!("Expected constraint object");
12408        };
12409        let Constraint::Radius(radius) = constraint else {
12410            panic!("Expected radius constraint");
12411        };
12412        assert_eq!(radius.label_position, Some(label_position));
12413
12414        mock_ctx.close().await;
12415    }
12416
12417    #[tokio::test(flavor = "multi_thread")]
12418    async fn test_vertical_distance_two_points() {
12419        let initial_source = "\
12420sketch(on = XY) {
12421  point(at = [var 1, var 2])
12422  point(at = [var 3, var 4])
12423}
12424";
12425
12426        let program = Program::parse(initial_source).unwrap().0.unwrap();
12427
12428        let mut frontend = FrontendState::new();
12429
12430        let mock_ctx = ExecutorContext::new_mock(None).await;
12431        let version = Version(0);
12432
12433        frontend.program = program.clone();
12434        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12435        frontend.update_state_after_exec(outcome, true);
12436        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12437        let sketch_id = sketch_object.id;
12438        let sketch = expect_sketch(sketch_object);
12439        let point0_id = *sketch.segments.first().unwrap();
12440        let point1_id = *sketch.segments.get(1).unwrap();
12441        let label_position = Point2d {
12442            x: Number {
12443                value: 10.0,
12444                units: NumericSuffix::Mm,
12445            },
12446            y: Number {
12447                value: 11.0,
12448                units: NumericSuffix::Mm,
12449            },
12450        };
12451
12452        let constraint = Constraint::VerticalDistance(Distance {
12453            segments: vec![point0_id.into(), point1_id.into()],
12454            distance: Number {
12455                value: 2.0,
12456                units: NumericSuffix::Mm,
12457            },
12458            label_position: Some(label_position.clone()),
12459            source: Default::default(),
12460        });
12461        let (src_delta, scene_delta) = frontend
12462            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12463            .await
12464            .unwrap();
12465        insta::assert_snapshot!("test_vertical_distance_two_points", src_delta.text.as_str());
12466        assert_eq!(
12467            scene_delta.new_graph.objects.len(),
12468            5,
12469            "{:#?}",
12470            scene_delta.new_graph.objects
12471        );
12472        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12473        let sketch = expect_sketch(sketch_object);
12474        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12475        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12476            panic!("Expected constraint object");
12477        };
12478        let Constraint::VerticalDistance(distance) = constraint else {
12479            panic!("Expected vertical distance constraint");
12480        };
12481        assert_eq!(distance.label_position, Some(label_position));
12482
12483        mock_ctx.close().await;
12484    }
12485
12486    #[tokio::test(flavor = "multi_thread")]
12487    async fn test_add_fixed_standalone_point() {
12488        let initial_source = "\
12489sketch(on = XY) {
12490  point(at = [var 1, var 2])
12491}
12492";
12493
12494        let program = Program::parse(initial_source).unwrap().0.unwrap();
12495
12496        let mut frontend = FrontendState::new();
12497
12498        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12499        let mock_ctx = ExecutorContext::new_mock(None).await;
12500        let version = Version(0);
12501
12502        frontend.hack_set_program(&ctx, program).await.unwrap();
12503        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12504        let sketch_id = sketch_object.id;
12505        let sketch = expect_sketch(sketch_object);
12506        let point_id = *sketch.segments.first().unwrap();
12507
12508        let (src_delta, scene_delta) = frontend
12509            .add_constraint(
12510                &mock_ctx,
12511                version,
12512                sketch_id,
12513                Constraint::Fixed(Fixed {
12514                    points: vec![FixedPoint {
12515                        point: point_id,
12516                        position: Point2d {
12517                            x: Number {
12518                                value: 2.0,
12519                                units: NumericSuffix::Mm,
12520                            },
12521                            y: Number {
12522                                value: 3.0,
12523                                units: NumericSuffix::Mm,
12524                            },
12525                        },
12526                    }],
12527                }),
12528            )
12529            .await
12530            .unwrap();
12531        insta::assert_snapshot!("test_add_fixed_standalone_point", src_delta.text.as_str());
12532        assert_eq!(
12533            scene_delta.new_graph.objects.len(),
12534            4,
12535            "{:#?}",
12536            scene_delta.new_graph.objects
12537        );
12538
12539        ctx.close().await;
12540        mock_ctx.close().await;
12541    }
12542
12543    #[tokio::test(flavor = "multi_thread")]
12544    async fn test_add_fixed_multiple_points() {
12545        let initial_source = "\
12546sketch(on = XY) {
12547  point(at = [var 1, var 2])
12548  point(at = [var 3, var 4])
12549}
12550";
12551
12552        let program = Program::parse(initial_source).unwrap().0.unwrap();
12553
12554        let mut frontend = FrontendState::new();
12555
12556        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12557        let mock_ctx = ExecutorContext::new_mock(None).await;
12558        let version = Version(0);
12559
12560        frontend.hack_set_program(&ctx, program).await.unwrap();
12561        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12562        let sketch_id = sketch_object.id;
12563        let sketch = expect_sketch(sketch_object);
12564        let point0_id = *sketch.segments.first().unwrap();
12565        let point1_id = *sketch.segments.get(1).unwrap();
12566
12567        let (src_delta, scene_delta) = frontend
12568            .add_constraint(
12569                &mock_ctx,
12570                version,
12571                sketch_id,
12572                Constraint::Fixed(Fixed {
12573                    points: vec![
12574                        FixedPoint {
12575                            point: point0_id,
12576                            position: Point2d {
12577                                x: Number {
12578                                    value: 2.0,
12579                                    units: NumericSuffix::Mm,
12580                                },
12581                                y: Number {
12582                                    value: 3.0,
12583                                    units: NumericSuffix::Mm,
12584                                },
12585                            },
12586                        },
12587                        FixedPoint {
12588                            point: point1_id,
12589                            position: Point2d {
12590                                x: Number {
12591                                    value: 4.0,
12592                                    units: NumericSuffix::Mm,
12593                                },
12594                                y: Number {
12595                                    value: 5.0,
12596                                    units: NumericSuffix::Mm,
12597                                },
12598                            },
12599                        },
12600                    ],
12601                }),
12602            )
12603            .await
12604            .unwrap();
12605        insta::assert_snapshot!("test_add_fixed_multiple_points", src_delta.text.as_str());
12606        assert_eq!(
12607            scene_delta.new_graph.objects.len(),
12608            6,
12609            "{:#?}",
12610            scene_delta.new_graph.objects
12611        );
12612
12613        ctx.close().await;
12614        mock_ctx.close().await;
12615    }
12616
12617    #[tokio::test(flavor = "multi_thread")]
12618    async fn test_add_fixed_owned_point() {
12619        let initial_source = "\
12620sketch(on = XY) {
12621  line(start = [var 1, var 2], end = [var 3, var 4])
12622}
12623";
12624
12625        let program = Program::parse(initial_source).unwrap().0.unwrap();
12626
12627        let mut frontend = FrontendState::new();
12628
12629        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12630        let mock_ctx = ExecutorContext::new_mock(None).await;
12631        let version = Version(0);
12632
12633        frontend.hack_set_program(&ctx, program).await.unwrap();
12634        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12635        let sketch_id = sketch_object.id;
12636        let sketch = expect_sketch(sketch_object);
12637        let line_start_id = *sketch.segments.first().unwrap();
12638
12639        let (src_delta, scene_delta) = frontend
12640            .add_constraint(
12641                &mock_ctx,
12642                version,
12643                sketch_id,
12644                Constraint::Fixed(Fixed {
12645                    points: vec![FixedPoint {
12646                        point: line_start_id,
12647                        position: Point2d {
12648                            x: Number {
12649                                value: 2.0,
12650                                units: NumericSuffix::Mm,
12651                            },
12652                            y: Number {
12653                                value: 3.0,
12654                                units: NumericSuffix::Mm,
12655                            },
12656                        },
12657                    }],
12658                }),
12659            )
12660            .await
12661            .unwrap();
12662        insta::assert_snapshot!("test_add_fixed_owned_point", src_delta.text.as_str());
12663        assert_eq!(
12664            scene_delta.new_graph.objects.len(),
12665            6,
12666            "{:#?}",
12667            scene_delta.new_graph.objects
12668        );
12669
12670        ctx.close().await;
12671        mock_ctx.close().await;
12672    }
12673
12674    #[tokio::test(flavor = "multi_thread")]
12675    async fn test_radius_error_cases() {
12676        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12677        let mock_ctx = ExecutorContext::new_mock(None).await;
12678        let version = Version(0);
12679
12680        // Test: Single point should error
12681        let initial_source_point = "\
12682sketch(on = XY) {
12683  point(at = [var 1, var 2])
12684}
12685";
12686        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12687        let mut frontend_point = FrontendState::new();
12688        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12689        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12690        let sketch_id_point = sketch_object_point.id;
12691        let sketch_point = expect_sketch(sketch_object_point);
12692        let point_id = *sketch_point.segments.first().unwrap();
12693
12694        let constraint_point = Constraint::Radius(Radius {
12695            arc: point_id,
12696            radius: Number {
12697                value: 5.0,
12698                units: NumericSuffix::Mm,
12699            },
12700            label_position: None,
12701            source: Default::default(),
12702        });
12703        let result_point = frontend_point
12704            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12705            .await;
12706        assert!(result_point.is_err(), "Single point should error for radius");
12707
12708        // Test: Single line segment should error (only arc segments supported)
12709        let initial_source_line = "\
12710sketch(on = XY) {
12711  line(start = [var 1, var 2], end = [var 3, var 4])
12712}
12713";
12714        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12715        let mut frontend_line = FrontendState::new();
12716        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12717        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12718        let sketch_id_line = sketch_object_line.id;
12719        let sketch_line = expect_sketch(sketch_object_line);
12720        let line_id = *sketch_line.segments.first().unwrap();
12721
12722        let constraint_line = Constraint::Radius(Radius {
12723            arc: line_id,
12724            radius: Number {
12725                value: 5.0,
12726                units: NumericSuffix::Mm,
12727            },
12728            label_position: None,
12729            source: Default::default(),
12730        });
12731        let result_line = frontend_line
12732            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12733            .await;
12734        assert!(result_line.is_err(), "Single line segment should error for radius");
12735
12736        ctx.close().await;
12737        mock_ctx.close().await;
12738    }
12739
12740    #[tokio::test(flavor = "multi_thread")]
12741    async fn test_diameter_single_arc_segment() {
12742        let initial_source = "\
12743sketch(on = XY) {
12744  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12745}
12746";
12747
12748        let program = Program::parse(initial_source).unwrap().0.unwrap();
12749
12750        let mut frontend = FrontendState::new();
12751
12752        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12753        let mock_ctx = ExecutorContext::new_mock(None).await;
12754        let version = Version(0);
12755
12756        frontend.hack_set_program(&ctx, program).await.unwrap();
12757        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12758        let sketch_id = sketch_object.id;
12759        let sketch = expect_sketch(sketch_object);
12760        // Find the arc segment (not the points)
12761        let arc_id = sketch
12762            .segments
12763            .iter()
12764            .find(|&seg_id| {
12765                let obj = frontend.scene_graph.objects.get(seg_id.0);
12766                matches!(
12767                    obj.map(|o| &o.kind),
12768                    Some(ObjectKind::Segment {
12769                        segment: Segment::Arc(_)
12770                    })
12771                )
12772            })
12773            .unwrap();
12774
12775        let constraint = Constraint::Diameter(Diameter {
12776            arc: *arc_id,
12777            diameter: Number {
12778                value: 10.0,
12779                units: NumericSuffix::Mm,
12780            },
12781            label_position: None,
12782            source: Default::default(),
12783        });
12784        let (src_delta, scene_delta) = frontend
12785            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12786            .await
12787            .unwrap();
12788        insta::assert_snapshot!("test_diameter_single_arc_segment", src_delta.text.as_str());
12789        assert_eq!(
12790            scene_delta.new_graph.objects.len(),
12791            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
12792            "{:#?}",
12793            scene_delta.new_graph.objects
12794        );
12795
12796        ctx.close().await;
12797        mock_ctx.close().await;
12798    }
12799
12800    #[tokio::test(flavor = "multi_thread")]
12801    async fn test_diameter_single_arc_segment_with_label_position() {
12802        let initial_source = "\
12803sketch(on = XY) {
12804  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12805}
12806";
12807
12808        let program = Program::parse(initial_source).unwrap().0.unwrap();
12809        let mut frontend = FrontendState::new();
12810        let mock_ctx = ExecutorContext::new_mock(None).await;
12811        let version = Version(0);
12812
12813        frontend.program = program.clone();
12814        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12815        frontend.update_state_after_exec(outcome, true);
12816        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12817        let sketch_id = sketch_object.id;
12818        let sketch = expect_sketch(sketch_object);
12819        let arc_id = sketch
12820            .segments
12821            .iter()
12822            .find(|&seg_id| {
12823                let obj = frontend.scene_graph.objects.get(seg_id.0);
12824                matches!(
12825                    obj.map(|o| &o.kind),
12826                    Some(ObjectKind::Segment {
12827                        segment: Segment::Arc(_)
12828                    })
12829                )
12830            })
12831            .unwrap();
12832
12833        let label_position = Point2d {
12834            x: Number {
12835                value: 10.0,
12836                units: NumericSuffix::Mm,
12837            },
12838            y: Number {
12839                value: 11.0,
12840                units: NumericSuffix::Mm,
12841            },
12842        };
12843        let constraint = Constraint::Diameter(Diameter {
12844            arc: *arc_id,
12845            diameter: Number {
12846                value: 10.0,
12847                units: NumericSuffix::Mm,
12848            },
12849            label_position: Some(label_position.clone()),
12850            source: Default::default(),
12851        });
12852        let (src_delta, scene_delta) = frontend
12853            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12854            .await
12855            .unwrap();
12856        insta::assert_snapshot!(
12857            "test_diameter_single_arc_segment_with_label_position",
12858            src_delta.text.as_str()
12859        );
12860
12861        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12862        let sketch = expect_sketch(sketch_object);
12863        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12864        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12865            panic!("Expected constraint object");
12866        };
12867        let Constraint::Diameter(diameter) = constraint else {
12868            panic!("Expected diameter constraint");
12869        };
12870        assert_eq!(diameter.label_position, Some(label_position));
12871
12872        mock_ctx.close().await;
12873    }
12874
12875    #[tokio::test(flavor = "multi_thread")]
12876    async fn test_edit_diameter_constraint_label_position() {
12877        let initial_source = "\
12878sketch(on = XY) {
12879  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12880  diameter(arc1) == 10mm
12881}
12882";
12883
12884        let program = Program::parse(initial_source).unwrap().0.unwrap();
12885        let mut frontend = FrontendState::new();
12886        let mock_ctx = ExecutorContext::new_mock(None).await;
12887        let version = Version(0);
12888
12889        frontend.program = program.clone();
12890        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12891        frontend.update_state_after_exec(outcome, true);
12892        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12893        let sketch_id = sketch_object.id;
12894        let sketch = expect_sketch(sketch_object);
12895        let constraint_id = sketch.constraints[0];
12896        let label_position = Point2d {
12897            x: Number {
12898                value: 10.0,
12899                units: NumericSuffix::Mm,
12900            },
12901            y: Number {
12902                value: 11.0,
12903                units: NumericSuffix::Mm,
12904            },
12905        };
12906
12907        let (src_delta, scene_delta) = frontend
12908            .edit_distance_constraint_label_position(
12909                &mock_ctx,
12910                version,
12911                sketch_id,
12912                constraint_id,
12913                label_position.clone(),
12914                vec![],
12915            )
12916            .await
12917            .unwrap();
12918        insta::assert_snapshot!("test_edit_diameter_constraint_label_position", src_delta.text.as_str());
12919
12920        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12921        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12922            panic!("Expected constraint object");
12923        };
12924        let Constraint::Diameter(diameter) = constraint else {
12925            panic!("Expected diameter constraint");
12926        };
12927        assert_eq!(diameter.label_position, Some(label_position));
12928
12929        mock_ctx.close().await;
12930    }
12931
12932    #[tokio::test(flavor = "multi_thread")]
12933    async fn test_diameter_error_cases() {
12934        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12935        let mock_ctx = ExecutorContext::new_mock(None).await;
12936        let version = Version(0);
12937
12938        // Test: Single point should error
12939        let initial_source_point = "\
12940sketch(on = XY) {
12941  point(at = [var 1, var 2])
12942}
12943";
12944        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12945        let mut frontend_point = FrontendState::new();
12946        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12947        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12948        let sketch_id_point = sketch_object_point.id;
12949        let sketch_point = expect_sketch(sketch_object_point);
12950        let point_id = *sketch_point.segments.first().unwrap();
12951
12952        let constraint_point = Constraint::Diameter(Diameter {
12953            arc: point_id,
12954            diameter: Number {
12955                value: 10.0,
12956                units: NumericSuffix::Mm,
12957            },
12958            label_position: None,
12959            source: Default::default(),
12960        });
12961        let result_point = frontend_point
12962            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12963            .await;
12964        assert!(result_point.is_err(), "Single point should error for diameter");
12965
12966        // Test: Single line segment should error (only arc segments supported)
12967        let initial_source_line = "\
12968sketch(on = XY) {
12969  line(start = [var 1, var 2], end = [var 3, var 4])
12970}
12971";
12972        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12973        let mut frontend_line = FrontendState::new();
12974        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12975        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12976        let sketch_id_line = sketch_object_line.id;
12977        let sketch_line = expect_sketch(sketch_object_line);
12978        let line_id = *sketch_line.segments.first().unwrap();
12979
12980        let constraint_line = Constraint::Diameter(Diameter {
12981            arc: line_id,
12982            diameter: Number {
12983                value: 10.0,
12984                units: NumericSuffix::Mm,
12985            },
12986            label_position: None,
12987            source: Default::default(),
12988        });
12989        let result_line = frontend_line
12990            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12991            .await;
12992        assert!(result_line.is_err(), "Single line segment should error for diameter");
12993
12994        ctx.close().await;
12995        mock_ctx.close().await;
12996    }
12997
12998    #[tokio::test(flavor = "multi_thread")]
12999    async fn test_line_horizontal() {
13000        let initial_source = "\
13001sketch(on = XY) {
13002  line(start = [var 1, var 2], end = [var 3, var 4])
13003}
13004";
13005
13006        let program = Program::parse(initial_source).unwrap().0.unwrap();
13007
13008        let mut frontend = FrontendState::new();
13009
13010        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13011        let mock_ctx = ExecutorContext::new_mock(None).await;
13012        let version = Version(0);
13013
13014        frontend.hack_set_program(&ctx, program).await.unwrap();
13015        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13016        let sketch_id = sketch_object.id;
13017        let sketch = expect_sketch(sketch_object);
13018        let line1_id = *sketch.segments.get(2).unwrap();
13019
13020        let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
13021        let (src_delta, scene_delta) = frontend
13022            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13023            .await
13024            .unwrap();
13025        insta::assert_snapshot!("test_line_horizontal", src_delta.text.as_str());
13026        assert_eq!(
13027            scene_delta.new_graph.objects.len(),
13028            6,
13029            "{:#?}",
13030            scene_delta.new_graph.objects
13031        );
13032
13033        ctx.close().await;
13034        mock_ctx.close().await;
13035    }
13036
13037    #[tokio::test(flavor = "multi_thread")]
13038    async fn test_control_point_spline_edge_horizontal() {
13039        let initial_source = "\
13040@settings(experimentalFeatures = allow)
13041splineSketch = sketch(on = XY) {
13042  controlPointSpline1 = controlPointSpline(points = [
13043    [var 0mm, var 0mm],
13044    [var 10mm, var 20mm],
13045    [var 20mm, var 0mm],
13046  ])
13047}
13048";
13049
13050        let program = Program::parse(initial_source).unwrap().0.unwrap();
13051
13052        let mut frontend = FrontendState::new();
13053
13054        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13055        let mock_ctx = ExecutorContext::new_mock(None).await;
13056        let version = Version(0);
13057
13058        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13059        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13060        let sketch_id = sketch_object.id;
13061        let sketch = expect_sketch(sketch_object);
13062        let spline_id = sketch
13063            .segments
13064            .iter()
13065            .copied()
13066            .find(|seg_id| {
13067                matches!(
13068                    &frontend.scene_graph.objects[seg_id.0].kind,
13069                    ObjectKind::Segment {
13070                        segment: Segment::ControlPointSpline(_)
13071                    }
13072                )
13073            })
13074            .expect("Expected a control point spline segment in sketch");
13075        let edge_id = frontend
13076            .scene_graph
13077            .objects
13078            .iter()
13079            .find_map(|obj| match &obj.kind {
13080                ObjectKind::Segment {
13081                    segment: Segment::Line(line),
13082                } if line.owner == Some(spline_id) => Some(obj.id),
13083                _ => None,
13084            })
13085            .expect("Expected an owned control-polygon edge");
13086
13087        let constraint = Constraint::Horizontal(Horizontal::Line { line: edge_id });
13088        let (src_delta, _) = frontend
13089            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13090            .await
13091            .unwrap();
13092        assert!(
13093            src_delta.text.contains("horizontal(controlPointSpline1.edges[0])"),
13094            "Expected horizontal constraint on spline edge, got: {}",
13095            src_delta.text
13096        );
13097
13098        ctx.close().await;
13099        mock_ctx.close().await;
13100    }
13101
13102    #[tokio::test(flavor = "multi_thread")]
13103    async fn test_control_point_spline_edge_angle() {
13104        let initial_source = "\
13105@settings(experimentalFeatures = allow)
13106splineSketch = sketch(on = XY) {
13107  controlPointSpline1 = controlPointSpline(points = [
13108    [var 0mm, var 0mm],
13109    [var 10mm, var 20mm],
13110    [var 20mm, var 0mm],
13111  ])
13112
13113  line1 = line(start = [var 40mm, var 0mm], end = [var 60mm, var 10mm])
13114}
13115";
13116
13117        let program = Program::parse(initial_source).unwrap().0.unwrap();
13118
13119        let mut frontend = FrontendState::new();
13120
13121        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13122        let mock_ctx = ExecutorContext::new_mock(None).await;
13123        let version = Version(0);
13124
13125        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13126        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13127        let sketch_id = sketch_object.id;
13128        let sketch = expect_sketch(sketch_object);
13129        let spline_id = sketch
13130            .segments
13131            .iter()
13132            .copied()
13133            .find(|seg_id| {
13134                matches!(
13135                    &frontend.scene_graph.objects[seg_id.0].kind,
13136                    ObjectKind::Segment {
13137                        segment: Segment::ControlPointSpline(_)
13138                    }
13139                )
13140            })
13141            .expect("Expected a control point spline segment in sketch");
13142        let edge_id = frontend
13143            .scene_graph
13144            .objects
13145            .iter()
13146            .find_map(|obj| match &obj.kind {
13147                ObjectKind::Segment {
13148                    segment: Segment::Line(line),
13149                } if line.owner == Some(spline_id) => Some(obj.id),
13150                _ => None,
13151            })
13152            .expect("Expected an owned control-polygon edge");
13153        let line1_id = frontend
13154            .scene_graph
13155            .objects
13156            .iter()
13157            .find_map(|obj| match &obj.kind {
13158                ObjectKind::Segment {
13159                    segment: Segment::Line(line),
13160                } if line.owner.is_none() && obj.label == "line1" => Some(obj.id),
13161                _ => None,
13162            })
13163            .or_else(|| {
13164                sketch.segments.iter().copied().find(|seg_id| {
13165                    matches!(
13166                        &frontend.scene_graph.objects[seg_id.0].kind,
13167                        ObjectKind::Segment {
13168                            segment: Segment::Line(line),
13169                        } if line.owner.is_none()
13170                    )
13171                })
13172            })
13173            .expect("Expected a standalone line segment in sketch");
13174
13175        let constraint = Constraint::Angle(Angle {
13176            lines: vec![line1_id, edge_id],
13177            angle: Number {
13178                value: 30.0,
13179                units: NumericSuffix::Deg,
13180            },
13181            sector: None,
13182            inverse: None,
13183            label_position: None,
13184            source: Default::default(),
13185        });
13186        let (src_delta, _) = frontend
13187            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13188            .await
13189            .unwrap();
13190        assert!(
13191            src_delta
13192                .text
13193                .contains("angle([line1, controlPointSpline1.edges[0]]) == 30deg"),
13194            "Expected angle constraint on spline edge, got: {}",
13195            src_delta.text
13196        );
13197
13198        ctx.close().await;
13199        mock_ctx.close().await;
13200    }
13201
13202    #[tokio::test(flavor = "multi_thread")]
13203    async fn test_ui_scene_graph_hides_same_spline_coincident_constraints() {
13204        let initial_source = "\
13205@settings(experimentalFeatures = allow)
13206splineSketch = sketch(on = XY) {
13207  spline1 = controlPointSpline(points = [
13208    [var 0mm, var 0mm],
13209    [var 10mm, var 20mm],
13210    [var 20mm, var 0mm],
13211  ])
13212  line1 = line(start = [var 0mm, var 0mm], end = [var -10mm, var 0mm])
13213  coincident([spline1.controls[1], spline1.edges[0]])
13214  coincident([spline1.controls[0], line1])
13215}
13216";
13217
13218        let program = Program::parse(initial_source).unwrap().0.unwrap();
13219
13220        let mut frontend = FrontendState::new();
13221
13222        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13223        let mock_ctx = ExecutorContext::new_mock(None).await;
13224
13225        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13226
13227        let ui_scene_graph = frontend.scene_graph_for_ui();
13228        let sketch_object = find_first_sketch_object(&ui_scene_graph).unwrap();
13229        let sketch = expect_sketch(sketch_object);
13230
13231        assert_eq!(
13232            sketch.constraints.len(),
13233            1,
13234            "Expected only the external coincident constraint to remain visible in the UI scene graph"
13235        );
13236
13237        let visible_constraints = ui_scene_graph
13238            .objects
13239            .iter()
13240            .filter_map(|object| match &object.kind {
13241                ObjectKind::Constraint {
13242                    constraint: Constraint::Coincident(coincident),
13243                } => Some(coincident.clone()),
13244                _ => None,
13245            })
13246            .collect::<Vec<_>>();
13247
13248        assert_eq!(
13249            visible_constraints.len(),
13250            1,
13251            "Expected only one coincident constraint object in the UI scene graph"
13252        );
13253        assert_eq!(
13254            visible_constraints[0].get_segments().len(),
13255            2,
13256            "Expected the remaining visible coincident constraint to reference two segments"
13257        );
13258
13259        ctx.close().await;
13260        mock_ctx.close().await;
13261    }
13262
13263    #[tokio::test(flavor = "multi_thread")]
13264    async fn test_edit_control_point_spline_can_append_control_point() {
13265        let initial_source = "\
13266@settings(experimentalFeatures = allow)
13267splineSketch = sketch(on = XY) {
13268  controlPointSpline(points = [
13269    [var 0mm, var 0mm],
13270    [var 10mm, var 20mm],
13271    [var 20mm, var 0mm],
13272  ])
13273}
13274";
13275
13276        let program = Program::parse(initial_source).unwrap().0.unwrap();
13277
13278        let mut frontend = FrontendState::new();
13279
13280        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13281        let mock_ctx = ExecutorContext::new_mock(None).await;
13282        let version = Version(0);
13283
13284        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13285        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13286        let sketch_id = sketch_object.id;
13287        let sketch = expect_sketch(sketch_object);
13288        let spline_id = sketch
13289            .segments
13290            .iter()
13291            .copied()
13292            .find(|seg_id| {
13293                matches!(
13294                    &frontend.scene_graph.objects[seg_id.0].kind,
13295                    ObjectKind::Segment {
13296                        segment: Segment::ControlPointSpline(_)
13297                    }
13298                )
13299            })
13300            .expect("Expected a control point spline segment in sketch");
13301
13302        let ctor = ControlPointSplineCtor {
13303            points: vec![
13304                Point2d {
13305                    x: Expr::Var(Number {
13306                        value: 0.0,
13307                        units: NumericSuffix::Mm,
13308                    }),
13309                    y: Expr::Var(Number {
13310                        value: 0.0,
13311                        units: NumericSuffix::Mm,
13312                    }),
13313                },
13314                Point2d {
13315                    x: Expr::Var(Number {
13316                        value: 10.0,
13317                        units: NumericSuffix::Mm,
13318                    }),
13319                    y: Expr::Var(Number {
13320                        value: 20.0,
13321                        units: NumericSuffix::Mm,
13322                    }),
13323                },
13324                Point2d {
13325                    x: Expr::Var(Number {
13326                        value: 20.0,
13327                        units: NumericSuffix::Mm,
13328                    }),
13329                    y: Expr::Var(Number {
13330                        value: 0.0,
13331                        units: NumericSuffix::Mm,
13332                    }),
13333                },
13334                Point2d {
13335                    x: Expr::Var(Number {
13336                        value: 30.0,
13337                        units: NumericSuffix::Mm,
13338                    }),
13339                    y: Expr::Var(Number {
13340                        value: 10.0,
13341                        units: NumericSuffix::Mm,
13342                    }),
13343                },
13344            ],
13345            construction: None,
13346        };
13347
13348        let segments = vec![ExistingSegmentCtor {
13349            id: spline_id,
13350            ctor: SegmentCtor::ControlPointSpline(ctor),
13351        }];
13352        let (src_delta, scene_delta) = frontend
13353            .edit_segments(&mock_ctx, version, sketch_id, segments)
13354            .await
13355            .unwrap();
13356
13357        assert!(
13358            src_delta.text.contains("[var 30mm, var 10mm]"),
13359            "Expected appended spline control point in source, got: {}",
13360            src_delta.text
13361        );
13362
13363        assert!(
13364            scene_delta.invalidates_ids,
13365            "Expected appending a spline control point to invalidate ids"
13366        );
13367        let updated_spline = scene_delta
13368            .new_graph
13369            .objects
13370            .iter()
13371            .find_map(|obj| match &obj.kind {
13372                ObjectKind::Segment {
13373                    segment: Segment::ControlPointSpline(updated_spline),
13374                } if updated_spline.controls.len() == 4 => Some(updated_spline),
13375                _ => None,
13376            })
13377            .expect("Expected edited scene graph to contain a four-point control point spline");
13378        assert_eq!(
13379            updated_spline.controls.len(),
13380            4,
13381            "Expected edited spline to expose four control points"
13382        );
13383
13384        ctx.close().await;
13385        mock_ctx.close().await;
13386    }
13387
13388    #[tokio::test(flavor = "multi_thread")]
13389    async fn test_line_vertical() {
13390        let initial_source = "\
13391sketch(on = XY) {
13392  line(start = [var 1, var 2], end = [var 3, var 4])
13393}
13394";
13395
13396        let program = Program::parse(initial_source).unwrap().0.unwrap();
13397
13398        let mut frontend = FrontendState::new();
13399
13400        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13401        let mock_ctx = ExecutorContext::new_mock(None).await;
13402        let version = Version(0);
13403
13404        frontend.hack_set_program(&ctx, program).await.unwrap();
13405        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13406        let sketch_id = sketch_object.id;
13407        let sketch = expect_sketch(sketch_object);
13408        let line1_id = *sketch.segments.get(2).unwrap();
13409
13410        let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
13411        let (src_delta, scene_delta) = frontend
13412            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13413            .await
13414            .unwrap();
13415        insta::assert_snapshot!("test_line_vertical", src_delta.text.as_str());
13416        assert_eq!(
13417            scene_delta.new_graph.objects.len(),
13418            6,
13419            "{:#?}",
13420            scene_delta.new_graph.objects
13421        );
13422
13423        ctx.close().await;
13424        mock_ctx.close().await;
13425    }
13426
13427    #[tokio::test(flavor = "multi_thread")]
13428    async fn test_points_vertical() {
13429        let initial_source = "\
13430sketch001 = sketch(on = XY) {
13431  p0 = point(at = [var -2.23mm, var 3.1mm])
13432  pf = point(at = [4, 4])
13433}
13434";
13435
13436        let program = Program::parse(initial_source).unwrap().0.unwrap();
13437
13438        let mut frontend = FrontendState::new();
13439
13440        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13441        let mock_ctx = ExecutorContext::new_mock(None).await;
13442        let version = Version(0);
13443
13444        frontend.hack_set_program(&ctx, program).await.unwrap();
13445        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13446        let sketch_id = sketch_object.id;
13447        let sketch = expect_sketch(sketch_object);
13448        let point_ids = vec![
13449            sketch.segments.first().unwrap().to_owned(),
13450            sketch.segments.get(1).unwrap().to_owned(),
13451        ];
13452
13453        let constraint = Constraint::Vertical(Vertical::Points {
13454            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
13455        });
13456        let (src_delta, scene_delta) = frontend
13457            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13458            .await
13459            .unwrap();
13460        insta::assert_snapshot!("test_points_vertical", src_delta.text.as_str());
13461        assert_eq!(
13462            scene_delta.new_graph.objects.len(),
13463            5,
13464            "{:#?}",
13465            scene_delta.new_graph.objects
13466        );
13467
13468        ctx.close().await;
13469        mock_ctx.close().await;
13470    }
13471
13472    #[tokio::test(flavor = "multi_thread")]
13473    async fn test_points_horizontal() {
13474        let initial_source = "\
13475sketch001 = sketch(on = XY) {
13476  p0 = point(at = [var -2.23mm, var 3.1mm])
13477  pf = point(at = [4, 4])
13478}
13479";
13480
13481        let program = Program::parse(initial_source).unwrap().0.unwrap();
13482
13483        let mut frontend = FrontendState::new();
13484
13485        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13486        let mock_ctx = ExecutorContext::new_mock(None).await;
13487        let version = Version(0);
13488
13489        frontend.hack_set_program(&ctx, program).await.unwrap();
13490        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13491        let sketch_id = sketch_object.id;
13492        let sketch = expect_sketch(sketch_object);
13493        let point_ids = vec![
13494            sketch.segments.first().unwrap().to_owned(),
13495            sketch.segments.get(1).unwrap().to_owned(),
13496        ];
13497
13498        let constraint = Constraint::Horizontal(Horizontal::Points {
13499            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
13500        });
13501        let (src_delta, scene_delta) = frontend
13502            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13503            .await
13504            .unwrap();
13505        insta::assert_snapshot!("test_points_horizontal", src_delta.text.as_str());
13506        assert_eq!(
13507            scene_delta.new_graph.objects.len(),
13508            5,
13509            "{:#?}",
13510            scene_delta.new_graph.objects
13511        );
13512
13513        ctx.close().await;
13514        mock_ctx.close().await;
13515    }
13516
13517    #[tokio::test(flavor = "multi_thread")]
13518    async fn test_point_horizontal_with_origin() {
13519        let initial_source = "\
13520sketch001 = sketch(on = XY) {
13521  p0 = point(at = [var -2.23mm, var 3.1mm])
13522}
13523";
13524
13525        let program = Program::parse(initial_source).unwrap().0.unwrap();
13526
13527        let mut frontend = FrontendState::new();
13528
13529        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13530        let mock_ctx = ExecutorContext::new_mock(None).await;
13531        let version = Version(0);
13532
13533        frontend.hack_set_program(&ctx, program).await.unwrap();
13534        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13535        let sketch_id = sketch_object.id;
13536        let sketch = expect_sketch(sketch_object);
13537        let point_id = *sketch.segments.first().unwrap();
13538
13539        let constraint = Constraint::Horizontal(Horizontal::Points {
13540            points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
13541        });
13542        let (src_delta, scene_delta) = frontend
13543            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13544            .await
13545            .unwrap();
13546        insta::assert_snapshot!("test_point_horizontal_with_origin", src_delta.text.as_str());
13547        assert_eq!(
13548            scene_delta.new_graph.objects.len(),
13549            4,
13550            "{:#?}",
13551            scene_delta.new_graph.objects
13552        );
13553
13554        ctx.close().await;
13555        mock_ctx.close().await;
13556    }
13557
13558    #[tokio::test(flavor = "multi_thread")]
13559    async fn test_lines_equal_length() {
13560        let initial_source = "\
13561sketch(on = XY) {
13562  line(start = [var 1, var 2], end = [var 3, var 4])
13563  line(start = [var 5, var 6], end = [var 7, var 8])
13564}
13565";
13566
13567        let program = Program::parse(initial_source).unwrap().0.unwrap();
13568
13569        let mut frontend = FrontendState::new();
13570
13571        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13572        let mock_ctx = ExecutorContext::new_mock(None).await;
13573        let version = Version(0);
13574
13575        frontend.hack_set_program(&ctx, program).await.unwrap();
13576        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13577        let sketch_id = sketch_object.id;
13578        let sketch = expect_sketch(sketch_object);
13579        let line1_id = *sketch.segments.get(2).unwrap();
13580        let line2_id = *sketch.segments.get(5).unwrap();
13581
13582        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13583            lines: vec![line1_id, line2_id],
13584        });
13585        let (src_delta, scene_delta) = frontend
13586            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13587            .await
13588            .unwrap();
13589        insta::assert_snapshot!("test_lines_equal_length", src_delta.text.as_str());
13590        assert_eq!(
13591            scene_delta.new_graph.objects.len(),
13592            9,
13593            "{:#?}",
13594            scene_delta.new_graph.objects
13595        );
13596
13597        ctx.close().await;
13598        mock_ctx.close().await;
13599    }
13600
13601    #[tokio::test(flavor = "multi_thread")]
13602    async fn test_add_constraint_multi_line_equal_length() {
13603        let initial_source = "\
13604sketch(on = XY) {
13605  line(start = [var 1, var 2], end = [var 3, var 4])
13606  line(start = [var 5, var 6], end = [var 7, var 8])
13607  line(start = [var 9, var 10], end = [var 11, var 12])
13608}
13609";
13610
13611        let program = Program::parse(initial_source).unwrap().0.unwrap();
13612
13613        let mut frontend = FrontendState::new();
13614        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13615        let mock_ctx = ExecutorContext::new_mock(None).await;
13616        let version = Version(0);
13617
13618        frontend.hack_set_program(&ctx, program).await.unwrap();
13619        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13620        let sketch_id = sketch_object.id;
13621        let sketch = expect_sketch(sketch_object);
13622        let line1_id = *sketch.segments.get(2).unwrap();
13623        let line2_id = *sketch.segments.get(5).unwrap();
13624        let line3_id = *sketch.segments.get(8).unwrap();
13625
13626        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13627            lines: vec![line1_id, line2_id, line3_id],
13628        });
13629        let (src_delta, scene_delta) = frontend
13630            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13631            .await
13632            .unwrap();
13633        insta::assert_snapshot!("test_add_constraint_multi_line_equal_length", src_delta.text.as_str());
13634        let constraints = scene_delta
13635            .new_graph
13636            .objects
13637            .iter()
13638            .filter_map(|obj| {
13639                let ObjectKind::Constraint { constraint } = &obj.kind else {
13640                    return None;
13641                };
13642                Some(constraint)
13643            })
13644            .collect::<Vec<_>>();
13645
13646        assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
13647        let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
13648            panic!("expected equal length constraint, got {:?}", constraints[0]);
13649        };
13650        assert_eq!(lines_equal_length.lines.len(), 3);
13651
13652        ctx.close().await;
13653        mock_ctx.close().await;
13654    }
13655
13656    #[tokio::test(flavor = "multi_thread")]
13657    async fn test_lines_parallel() {
13658        let initial_source = "\
13659sketch(on = XY) {
13660  line(start = [var 1, var 2], end = [var 3, var 4])
13661  line(start = [var 5, var 6], end = [var 7, var 8])
13662}
13663";
13664
13665        let program = Program::parse(initial_source).unwrap().0.unwrap();
13666
13667        let mut frontend = FrontendState::new();
13668
13669        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13670        let mock_ctx = ExecutorContext::new_mock(None).await;
13671        let version = Version(0);
13672
13673        frontend.hack_set_program(&ctx, program).await.unwrap();
13674        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13675        let sketch_id = sketch_object.id;
13676        let sketch = expect_sketch(sketch_object);
13677        let line1_id = *sketch.segments.get(2).unwrap();
13678        let line2_id = *sketch.segments.get(5).unwrap();
13679
13680        let constraint = Constraint::Parallel(Parallel {
13681            lines: vec![line1_id, line2_id],
13682        });
13683        let (src_delta, scene_delta) = frontend
13684            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13685            .await
13686            .unwrap();
13687        insta::assert_snapshot!("test_lines_parallel", src_delta.text.as_str());
13688        assert_eq!(
13689            scene_delta.new_graph.objects.len(),
13690            9,
13691            "{:#?}",
13692            scene_delta.new_graph.objects
13693        );
13694
13695        ctx.close().await;
13696        mock_ctx.close().await;
13697    }
13698
13699    #[tokio::test(flavor = "multi_thread")]
13700    async fn test_lines_parallel_multiline() {
13701        let initial_source = "\
13702sketch(on = XY) {
13703  line(start = [var 1, var 2], end = [var 3, var 4])
13704  line(start = [var 5, var 6], end = [var 7, var 8])
13705  line(start = [var 9, var 10], end = [var 11, var 12])
13706}
13707";
13708
13709        let program = Program::parse(initial_source).unwrap().0.unwrap();
13710
13711        let mut frontend = FrontendState::new();
13712
13713        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13714        let mock_ctx = ExecutorContext::new_mock(None).await;
13715        let version = Version(0);
13716
13717        frontend.hack_set_program(&ctx, program).await.unwrap();
13718        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13719        let sketch_id = sketch_object.id;
13720        let sketch = expect_sketch(sketch_object);
13721        let line1_id = *sketch.segments.get(2).unwrap();
13722        let line2_id = *sketch.segments.get(5).unwrap();
13723        let line3_id = *sketch.segments.get(8).unwrap();
13724
13725        let constraint = Constraint::Parallel(Parallel {
13726            lines: vec![line1_id, line2_id, line3_id],
13727        });
13728        let (src_delta, scene_delta) = frontend
13729            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13730            .await
13731            .unwrap();
13732        insta::assert_snapshot!("test_lines_parallel_multiline", src_delta.text.as_str());
13733
13734        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
13735        let sketch = expect_sketch(sketch_object);
13736        assert_eq!(sketch.constraints.len(), 1);
13737
13738        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
13739        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
13740            panic!("Expected constraint object");
13741        };
13742        let Constraint::Parallel(parallel) = constraint else {
13743            panic!("Expected parallel constraint");
13744        };
13745        assert_eq!(parallel.lines.len(), 3);
13746
13747        ctx.close().await;
13748        mock_ctx.close().await;
13749    }
13750
13751    #[tokio::test(flavor = "multi_thread")]
13752    async fn test_lines_perpendicular() {
13753        let initial_source = "\
13754sketch(on = XY) {
13755  line(start = [var 1, var 2], end = [var 3, var 4])
13756  line(start = [var 5, var 6], end = [var 7, var 8])
13757}
13758";
13759
13760        let program = Program::parse(initial_source).unwrap().0.unwrap();
13761
13762        let mut frontend = FrontendState::new();
13763
13764        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13765        let mock_ctx = ExecutorContext::new_mock(None).await;
13766        let version = Version(0);
13767
13768        frontend.hack_set_program(&ctx, program).await.unwrap();
13769        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13770        let sketch_id = sketch_object.id;
13771        let sketch = expect_sketch(sketch_object);
13772        let line1_id = *sketch.segments.get(2).unwrap();
13773        let line2_id = *sketch.segments.get(5).unwrap();
13774
13775        let constraint = Constraint::Perpendicular(Perpendicular {
13776            lines: vec![line1_id, line2_id],
13777        });
13778        let (src_delta, scene_delta) = frontend
13779            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13780            .await
13781            .unwrap();
13782        insta::assert_snapshot!("test_lines_perpendicular", src_delta.text.as_str());
13783        assert_eq!(
13784            scene_delta.new_graph.objects.len(),
13785            9,
13786            "{:#?}",
13787            scene_delta.new_graph.objects
13788        );
13789
13790        ctx.close().await;
13791        mock_ctx.close().await;
13792    }
13793
13794    #[tokio::test(flavor = "multi_thread")]
13795    async fn test_lines_angle() {
13796        let initial_source = "\
13797sketch(on = XY) {
13798  line(start = [var 1, var 2], end = [var 3, var 4])
13799  line(start = [var 5, var 6], end = [var 7, var 8])
13800}
13801";
13802
13803        let program = Program::parse(initial_source).unwrap().0.unwrap();
13804
13805        let mut frontend = FrontendState::new();
13806
13807        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13808        let mock_ctx = ExecutorContext::new_mock(None).await;
13809        let version = Version(0);
13810
13811        frontend.hack_set_program(&ctx, program).await.unwrap();
13812        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13813        let sketch_id = sketch_object.id;
13814        let sketch = expect_sketch(sketch_object);
13815        let line1_id = *sketch.segments.get(2).unwrap();
13816        let line2_id = *sketch.segments.get(5).unwrap();
13817
13818        let constraint = Constraint::Angle(Angle {
13819            lines: vec![line1_id, line2_id],
13820            angle: Number {
13821                value: 30.0,
13822                units: NumericSuffix::Deg,
13823            },
13824            sector: None,
13825            inverse: None,
13826            label_position: None,
13827            source: Default::default(),
13828        });
13829        let (src_delta, scene_delta) = frontend
13830            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13831            .await
13832            .unwrap();
13833        insta::assert_snapshot!("test_lines_angle", src_delta.text.as_str());
13834        assert_eq!(
13835            scene_delta.new_graph.objects.len(),
13836            9,
13837            "{:#?}",
13838            scene_delta.new_graph.objects
13839        );
13840
13841        ctx.close().await;
13842        mock_ctx.close().await;
13843    }
13844
13845    #[tokio::test(flavor = "multi_thread")]
13846    async fn test_lines_angle_with_sector_uses_angle_dimension() {
13847        let initial_source = "\
13848sketch(on = XY) {
13849  line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
13850  line(start = [var 0mm, var 0mm], end = [var 0mm, var 4mm])
13851}
13852";
13853
13854        let program = Program::parse(initial_source).unwrap().0.unwrap();
13855
13856        let mut frontend = FrontendState::new();
13857
13858        let mock_ctx = ExecutorContext::new_mock(None).await;
13859        let version = Version(0);
13860
13861        frontend.program = program.clone();
13862        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13863        frontend.update_state_after_exec(outcome, true);
13864        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13865        let sketch_id = sketch_object.id;
13866        let sketch = expect_sketch(sketch_object);
13867        let line1_id = *sketch.segments.get(2).unwrap();
13868        let line2_id = *sketch.segments.get(5).unwrap();
13869
13870        let constraint = Constraint::Angle(Angle {
13871            lines: vec![line1_id, line2_id],
13872            angle: Number {
13873                value: 270.0,
13874                units: NumericSuffix::Deg,
13875            },
13876            sector: Some(1),
13877            inverse: Some(true),
13878            label_position: Some(Point2d {
13879                x: Number {
13880                    value: -0.73,
13881                    units: NumericSuffix::Mm,
13882                },
13883                y: Number {
13884                    value: 0.75,
13885                    units: NumericSuffix::Mm,
13886                },
13887            }),
13888            source: Default::default(),
13889        });
13890        let (src_delta, _) = frontend
13891            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13892            .await
13893            .unwrap();
13894        assert_eq!(
13895            src_delta.text.as_str(),
13896            "\
13897sketch(on = XY) {
13898  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
13899  line2 = line(start = [var 0mm, var 0mm], end = [var 0mm, var 4mm])
13900  angleDimension(
13901  lines = [line1, line2],
13902  sector = 1,
13903  inverse = true,
13904  labelPosition = [-0.73mm, 0.75mm],
13905) == 270deg
13906}
13907"
13908        );
13909
13910        mock_ctx.close().await;
13911    }
13912
13913    #[tokio::test(flavor = "multi_thread")]
13914    async fn test_segments_tangent() {
13915        let initial_source = "\
13916sketch(on = XY) {
13917  line(start = [var 1, var 2], end = [var 3, var 4])
13918  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13919}
13920";
13921
13922        let program = Program::parse(initial_source).unwrap().0.unwrap();
13923
13924        let mut frontend = FrontendState::new();
13925
13926        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13927        let mock_ctx = ExecutorContext::new_mock(None).await;
13928        let version = Version(0);
13929
13930        frontend.hack_set_program(&ctx, program).await.unwrap();
13931        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13932        let sketch_id = sketch_object.id;
13933        let sketch = expect_sketch(sketch_object);
13934        let line1_id = *sketch.segments.get(2).unwrap();
13935        let arc1_id = *sketch.segments.get(6).unwrap();
13936
13937        let constraint = Constraint::Tangent(Tangent {
13938            input: vec![line1_id, arc1_id],
13939        });
13940        let (src_delta, scene_delta) = frontend
13941            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13942            .await
13943            .unwrap();
13944        insta::assert_snapshot!("test_segments_tangent", src_delta.text.as_str());
13945        assert_eq!(
13946            scene_delta.new_graph.objects.len(),
13947            10,
13948            "{:#?}",
13949            scene_delta.new_graph.objects
13950        );
13951
13952        ctx.close().await;
13953        mock_ctx.close().await;
13954    }
13955
13956    #[tokio::test(flavor = "multi_thread")]
13957    async fn test_point_midpoint() {
13958        let initial_source = "\
13959sketch(on = XY) {
13960  point(at = [var 1, var 1])
13961  line(start = [var 0, var 0], end = [var 6, var 4])
13962}
13963";
13964
13965        let program = Program::parse(initial_source).unwrap().0.unwrap();
13966
13967        let mut frontend = FrontendState::new();
13968
13969        let ctx = ExecutorContext::new_mock(None).await;
13970        let version = Version(0);
13971
13972        frontend.program = program.clone();
13973        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13974        frontend.update_state_after_exec(outcome, true);
13975        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13976        let sketch_id = sketch_object.id;
13977        let sketch = expect_sketch(sketch_object);
13978        let point_id = *sketch.segments.first().unwrap();
13979        let line_id = *sketch.segments.get(3).unwrap();
13980
13981        let constraint = Constraint::Midpoint(Midpoint {
13982            point: ConstraintSegment::from(point_id),
13983            segment: line_id,
13984        });
13985        let (src_delta, scene_delta) = frontend
13986            .add_constraint(&ctx, version, sketch_id, constraint)
13987            .await
13988            .unwrap();
13989        insta::assert_snapshot!("test_point_midpoint", src_delta.text.as_str());
13990        assert_eq!(
13991            scene_delta.new_graph.objects.len(),
13992            7,
13993            "{:#?}",
13994            scene_delta.new_graph.objects
13995        );
13996
13997        ctx.close().await;
13998    }
13999
14000    #[tokio::test(flavor = "multi_thread")]
14001    async fn test_segments_symmetric() {
14002        let initial_source = "\
14003sketch(on = XY) {
14004  line(start = [var 0, var 0], end = [var 0, var 4])
14005  line(start = [var 4, var 0], end = [var 4, var 4])
14006  line(start = [var 2, var -1], end = [var 2, var 5])
14007}
14008";
14009
14010        let program = Program::parse(initial_source).unwrap().0.unwrap();
14011
14012        let mut frontend = FrontendState::new();
14013
14014        let ctx = ExecutorContext::new_mock(None).await;
14015        let version = Version(0);
14016
14017        frontend.program = program.clone();
14018        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14019        frontend.update_state_after_exec(outcome, true);
14020        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14021        let sketch_id = sketch_object.id;
14022        let sketch = expect_sketch(sketch_object);
14023        let line1_id = *sketch.segments.get(2).unwrap();
14024        let line2_id = *sketch.segments.get(5).unwrap();
14025        let axis_id = *sketch.segments.get(8).unwrap();
14026
14027        let constraint = Constraint::Symmetric(Symmetric {
14028            input: vec![line1_id, line2_id],
14029            axis: axis_id,
14030        });
14031        let (src_delta, scene_delta) = frontend
14032            .add_constraint(&ctx, version, sketch_id, constraint)
14033            .await
14034            .unwrap();
14035        insta::assert_snapshot!("test_segments_symmetric", src_delta.text.as_str());
14036        assert_eq!(
14037            scene_delta.new_graph.objects.len(),
14038            12,
14039            "{:#?}",
14040            scene_delta.new_graph.objects
14041        );
14042
14043        ctx.close().await;
14044    }
14045
14046    #[tokio::test(flavor = "multi_thread")]
14047    async fn test_point_arc_midpoint() {
14048        let initial_source = "\
14049sketch(on = XY) {
14050  point(at = [var 6, var 3])
14051  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
14052}
14053";
14054
14055        let program = Program::parse(initial_source).unwrap().0.unwrap();
14056
14057        let mut frontend = FrontendState::new();
14058
14059        let ctx = ExecutorContext::new_mock(None).await;
14060        let version = Version(0);
14061
14062        frontend.program = program.clone();
14063        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14064        frontend.update_state_after_exec(outcome, true);
14065        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14066        let sketch_id = sketch_object.id;
14067        let sketch = expect_sketch(sketch_object);
14068        let point_id = *sketch.segments.first().unwrap();
14069        let arc_id = *sketch.segments.get(4).unwrap();
14070
14071        let constraint = Constraint::Midpoint(Midpoint {
14072            point: ConstraintSegment::from(point_id),
14073            segment: arc_id,
14074        });
14075        let (src_delta, scene_delta) = frontend
14076            .add_constraint(&ctx, version, sketch_id, constraint)
14077            .await
14078            .unwrap();
14079        insta::assert_snapshot!("test_point_arc_midpoint", src_delta.text.as_str());
14080        assert_eq!(
14081            scene_delta.new_graph.objects.len(),
14082            8,
14083            "{:#?}",
14084            scene_delta.new_graph.objects
14085        );
14086
14087        ctx.close().await;
14088    }
14089
14090    #[tokio::test(flavor = "multi_thread")]
14091    async fn test_origin_line_midpoint() {
14092        let initial_source = "\
14093sketch(on = XY) {
14094  line(start = [var 0, var 0], end = [var 6, var 4])
14095}
14096";
14097
14098        let program = Program::parse(initial_source).unwrap().0.unwrap();
14099
14100        let mut frontend = FrontendState::new();
14101
14102        let ctx = ExecutorContext::new_mock(None).await;
14103        let version = Version(0);
14104
14105        frontend.program = program.clone();
14106        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14107        frontend.update_state_after_exec(outcome, true);
14108        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14109        let sketch_id = sketch_object.id;
14110        let sketch = expect_sketch(sketch_object);
14111        let line_id = *sketch.segments.get(2).unwrap();
14112
14113        let constraint = Constraint::Midpoint(Midpoint {
14114            point: ConstraintSegment::ORIGIN,
14115            segment: line_id,
14116        });
14117        let (src_delta, scene_delta) = frontend
14118            .add_constraint(&ctx, version, sketch_id, constraint)
14119            .await
14120            .unwrap();
14121        insta::assert_snapshot!("test_origin_line_midpoint", src_delta.text.as_str());
14122        assert_eq!(
14123            scene_delta.new_graph.objects.len(),
14124            6,
14125            "{:#?}",
14126            scene_delta.new_graph.objects
14127        );
14128
14129        ctx.close().await;
14130    }
14131
14132    #[tokio::test(flavor = "multi_thread")]
14133    async fn test_origin_arc_midpoint() {
14134        let initial_source = "\
14135sketch(on = XY) {
14136  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
14137}
14138";
14139
14140        let program = Program::parse(initial_source).unwrap().0.unwrap();
14141
14142        let mut frontend = FrontendState::new();
14143
14144        let ctx = ExecutorContext::new_mock(None).await;
14145        let version = Version(0);
14146
14147        frontend.program = program.clone();
14148        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14149        frontend.update_state_after_exec(outcome, true);
14150        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14151        let sketch_id = sketch_object.id;
14152        let sketch = expect_sketch(sketch_object);
14153        let arc_id = *sketch.segments.get(3).unwrap();
14154
14155        let constraint = Constraint::Midpoint(Midpoint {
14156            point: ConstraintSegment::ORIGIN,
14157            segment: arc_id,
14158        });
14159        let (src_delta, scene_delta) = frontend
14160            .add_constraint(&ctx, version, sketch_id, constraint)
14161            .await
14162            .unwrap();
14163        insta::assert_snapshot!("test_origin_arc_midpoint", src_delta.text.as_str());
14164        assert_eq!(
14165            scene_delta.new_graph.objects.len(),
14166            7,
14167            "{:#?}",
14168            scene_delta.new_graph.objects
14169        );
14170
14171        ctx.close().await;
14172    }
14173
14174    #[tokio::test(flavor = "multi_thread")]
14175    async fn test_segments_symmetric_arcs() {
14176        let initial_source = "\
14177sketch(on = XY) {
14178  arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
14179  arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
14180  line(start = [var 0, var -10], end = [var 0, var 10])
14181}
14182";
14183
14184        let program = Program::parse(initial_source).unwrap().0.unwrap();
14185
14186        let mut frontend = FrontendState::new();
14187
14188        let ctx = ExecutorContext::new_mock(None).await;
14189        let version = Version(0);
14190
14191        frontend.program = program.clone();
14192        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14193        frontend.update_state_after_exec(outcome, true);
14194        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14195        let sketch_id = sketch_object.id;
14196        let sketch = expect_sketch(sketch_object);
14197        let arc1_id = *sketch.segments.get(3).unwrap();
14198        let arc2_id = *sketch.segments.get(7).unwrap();
14199        let axis_id = *sketch.segments.get(10).unwrap();
14200
14201        let constraint = Constraint::Symmetric(Symmetric {
14202            input: vec![arc1_id, arc2_id],
14203            axis: axis_id,
14204        });
14205        let (src_delta, scene_delta) = frontend
14206            .add_constraint(&ctx, version, sketch_id, constraint)
14207            .await
14208            .unwrap();
14209        insta::assert_snapshot!("test_segments_symmetric_arcs", src_delta.text.as_str());
14210        assert_eq!(
14211            scene_delta.new_graph.objects.len(),
14212            14,
14213            "{:#?}",
14214            scene_delta.new_graph.objects
14215        );
14216
14217        ctx.close().await;
14218    }
14219
14220    #[tokio::test(flavor = "multi_thread")]
14221    async fn test_sketch_on_face_simple() {
14222        let initial_source = "\
14223len = 2mm
14224cube = startSketchOn(XY)
14225  |> startProfile(at = [0, 0])
14226  |> line(end = [len, 0], tag = $side)
14227  |> line(end = [0, len])
14228  |> line(end = [-len, 0])
14229  |> line(end = [0, -len])
14230  |> close()
14231  |> extrude(length = len)
14232
14233face = faceOf(cube, face = side)
14234";
14235
14236        let program = Program::parse(initial_source).unwrap().0.unwrap();
14237
14238        let mut frontend = FrontendState::new();
14239
14240        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14241        let mock_ctx = ExecutorContext::new_mock(None).await;
14242        let version = Version(0);
14243
14244        frontend.hack_set_program(&ctx, program).await.unwrap();
14245        let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
14246        let face_id = face_object.id;
14247
14248        let sketch_args = SketchCtor {
14249            on: Plane::Object(face_id),
14250        };
14251        let (_src_delta, scene_delta, sketch_id) = frontend
14252            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14253            .await
14254            .unwrap();
14255        assert_eq!(sketch_id, ObjectId(2));
14256        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14257        let sketch_object = &scene_delta.new_graph.objects[2];
14258        assert_eq!(sketch_object.id, ObjectId(2));
14259        assert_eq!(
14260            sketch_object.kind,
14261            ObjectKind::Sketch(Sketch {
14262                args: SketchCtor {
14263                    on: Plane::Object(face_id),
14264                },
14265                plane: face_id,
14266                segments: vec![],
14267                constraints: vec![],
14268            })
14269        );
14270        assert_eq!(scene_delta.new_graph.objects.len(), 8);
14271
14272        ctx.close().await;
14273        mock_ctx.close().await;
14274    }
14275
14276    #[tokio::test(flavor = "multi_thread")]
14277    async fn test_new_sketch_on_primitive_index_face() {
14278        let initial_source = "\
14279@settings(kclVersion = 2.0)
14280
14281sketch001 = sketch(on = XY) {
14282  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
14283}
14284extrude001 = extrude(region(point = [0mm, 0mm], sketch = sketch001), length = 5, tagEnd = $capEnd001)
14285shell001 = shell(extrude001, faces = capEnd001, thickness = 1)";
14286        let program = Program::parse(initial_source).unwrap().0.unwrap();
14287        let ctx = ExecutorContext::new_mock(None).await;
14288        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14289        let solid_id = match outcome.variables.get("shell001") {
14290            Some(KclValueView::Solid { value }) => value.id,
14291            value => panic!("expected shell001 to be a solid, got {value:?}"),
14292        };
14293        let solid_references = solid_references_from_variables(&program.ast, &outcome.variables);
14294
14295        let mut ast = program.ast;
14296        let scene_graph = SceneGraph::empty(ProjectId(0), FileId(0), Version(0));
14297        let face_expr = sketch_on_ast_expr(
14298            &mut ast,
14299            &scene_graph,
14300            &solid_references,
14301            &Plane::PrimitiveFace(crate::frontend::api::PrimitiveFacePlane { solid_id, index: 6 }),
14302        )
14303        .unwrap();
14304        let face_decl = ast::VariableDeclaration::new(
14305            ast::VariableDeclarator::new("face001", face_expr),
14306            ast::ItemVisibility::Default,
14307            ast::VariableKind::Const,
14308        );
14309        ast.body
14310            .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
14311                face_decl,
14312            ))));
14313        let face_source = source_from_ast(&ast);
14314        let new_source = format!("{face_source}sketch002 = sketch(on = face001) {{\n}}\n");
14315        insta::assert_snapshot!("test_new_sketch_on_primitive_index_face", new_source);
14316
14317        let program = Program::parse(&new_source).unwrap().0.unwrap();
14318        ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14319        ctx.close().await;
14320    }
14321
14322    #[tokio::test(flavor = "multi_thread")]
14323    async fn test_sketch_on_wall_artifact_from_region_extrude() {
14324        let initial_source = "\
14325s = sketch(on = YZ) {
14326  line1 = line(start = [0, 0], end = [0, 1])
14327  line2 = line(start = [0, 1], end = [1, 1])
14328  line3 = line(start = [1, 1], end = [0, 0])
14329}
14330region001 = region(point = [0.1, 0.1], sketch = s)
14331extrude001 = extrude(region001, length = 5)
14332";
14333
14334        let program = Program::parse(initial_source).unwrap().0.unwrap();
14335
14336        let mut frontend = FrontendState::new();
14337        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14338        let version = Version(0);
14339
14340        frontend.hack_set_program(&ctx, program).await.unwrap();
14341        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
14342
14343        let sketch_args = SketchCtor {
14344            on: Plane::Object(wall_object_id),
14345        };
14346        let (src_delta, _scene_delta, _sketch_id) = frontend
14347            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14348            .await
14349            .unwrap();
14350        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
14351
14352        ctx.close().await;
14353    }
14354
14355    #[tokio::test(flavor = "multi_thread")]
14356    async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
14357        let initial_source = "\
14358sketch001 = sketch(on = YZ) {
14359  line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
14360  line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
14361  line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
14362  line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
14363  coincident([line1.end, line2.start])
14364  coincident([line2.end, line3.start])
14365  coincident([line3.end, line4.start])
14366  coincident([line4.end, line1.start])
14367  parallel([line2, line4])
14368  parallel([line3, line1])
14369  perpendicular([line1, line2])
14370  horizontal(line3)
14371  line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
14372}
14373region001 = region(point = [3.1, 3.74], sketch = sketch001)
14374extrude001 = extrude(region001, length = 5)
14375";
14376
14377        let program = Program::parse(initial_source).unwrap().0.unwrap();
14378
14379        let mut frontend = FrontendState::new();
14380        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14381        let version = Version(0);
14382
14383        frontend.hack_set_program(&ctx, program).await.unwrap();
14384        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
14385
14386        let sketch_args = SketchCtor {
14387            on: Plane::Object(wall_object_id),
14388        };
14389        let (src_delta, _scene_delta, _sketch_id) = frontend
14390            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14391            .await
14392            .unwrap();
14393        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
14394
14395        ctx.close().await;
14396    }
14397
14398    #[tokio::test(flavor = "multi_thread")]
14399    async fn test_new_sketch_on_multi_region_extrude_cap_indexes_selected_solid() {
14400        let initial_source = "\
14401@settings(kclVersion = 2.0)
14402
14403sketch001 = sketch(on = XY) {
14404  circle1 = circle(start = [var -1.51mm, var 1.31mm], center = [var -1.98mm, var 1.03mm])
14405  circle2 = circle(start = [var 2.98mm, var 2.37mm], center = [var 2.66mm, var 1.46mm])
14406}
14407hidden001 = hide(sketch001)
14408region001 = region(segments = [sketch001.circle2])
14409region002 = region(segments = [sketch001.circle1])
14410extrude001 = extrude([region001, region002], length = 5)
14411";
14412
14413        let program = Program::parse(initial_source).unwrap().0.unwrap();
14414        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14415        let version = Version(0);
14416
14417        for (solid_output_index, expected_face) in [
14418            (0, "faceOf(extrude001[0], face = END)"),
14419            (1, "faceOf(extrude001[1], face = END)"),
14420        ] {
14421            let mut frontend = FrontendState::new();
14422            frontend.hack_set_program(&ctx, program.clone()).await.unwrap();
14423            let cap_object_id = find_cap_object_id_with_solid_output_index(
14424                &frontend.scene_graph,
14425                crate::frontend::api::CapKind::End,
14426                solid_output_index,
14427            )
14428            .unwrap_or_else(|| panic!("expected an end cap object for solid output index {solid_output_index}"));
14429
14430            let sketch_args = SketchCtor {
14431                on: Plane::Object(cap_object_id),
14432            };
14433            let (src_delta, _scene_delta, _sketch_id) = frontend
14434                .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14435                .await
14436                .unwrap();
14437
14438            assert!(
14439                src_delta.text.contains(expected_face),
14440                "expected `{expected_face}` in:\n{}",
14441                src_delta.text
14442            );
14443            assert!(!src_delta.text.contains("faceOf(extrude001, face = END)"));
14444        }
14445
14446        ctx.close().await;
14447    }
14448
14449    #[tokio::test(flavor = "multi_thread")]
14450    async fn test_new_sketch_on_multi_region_extrude_wall_indexes_selected_solid() {
14451        let initial_source = "\
14452@settings(kclVersion = 2.0)
14453
14454sketch001 = sketch(on = XY) {
14455  rect1Line1 = line(start = [0, 0], end = [1, 0])
14456  rect1Line2 = line(start = [1, 0], end = [1, 1])
14457  rect1Line3 = line(start = [1, 1], end = [0, 1])
14458  rect1Line4 = line(start = [0, 1], end = [0, 0])
14459  rect2Line1 = line(start = [3, 0], end = [4, 0])
14460  rect2Line2 = line(start = [4, 0], end = [4, 1])
14461  rect2Line3 = line(start = [4, 1], end = [3, 1])
14462  rect2Line4 = line(start = [3, 1], end = [3, 0])
14463}
14464hidden001 = hide(sketch001)
14465region001 = region(segments = [
14466  sketch001.rect1Line4,
14467  sketch001.rect1Line1
14468])
14469region002 = region(segments = [
14470  sketch001.rect2Line4,
14471  sketch001.rect2Line1
14472])
14473extrude001 = extrude([region001, region002], length = 5)
14474";
14475
14476        let program = Program::parse(initial_source).unwrap().0.unwrap();
14477        let mut frontend = FrontendState::new();
14478        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14479        let version = Version(0);
14480
14481        frontend.hack_set_program(&ctx, program).await.unwrap();
14482        let region_call = "\
14483region(segments = [
14484  sketch001.rect1Line4,
14485  sketch001.rect1Line1
14486])";
14487        let region_call_start = initial_source.find(region_call).unwrap();
14488        let region_range = [region_call_start, region_call_start + region_call.len(), 0].into();
14489        let segment_call = "line(start = [0, 0], end = [1, 0])";
14490        let segment_call_start = initial_source.find(segment_call).unwrap();
14491        let segment_range = [segment_call_start, segment_call_start + segment_call.len(), 0].into();
14492        let wall_object_id = frontend
14493            .scene_graph
14494            .objects
14495            .iter()
14496            .find_map(|object| match &object.kind {
14497                ObjectKind::Wall(wall)
14498                    if wall.source.path.as_ref().is_some_and(|path| path.range == region_range)
14499                        && wall.source.segment.range == segment_range =>
14500                {
14501                    Some(object.id)
14502                }
14503                _ => None,
14504            })
14505            .expect("expected a wall object for region001.tags.rect1Line1");
14506
14507        let sketch_args = SketchCtor {
14508            on: Plane::Object(wall_object_id),
14509        };
14510        let (src_delta, _scene_delta, _sketch_id) = frontend
14511            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14512            .await
14513            .unwrap();
14514
14515        let expected_face = "faceOf(extrude001[0], face = region001.tags.rect1Line1)";
14516        assert!(
14517            src_delta.text.contains(expected_face),
14518            "expected `{expected_face}` in:\n{}",
14519            src_delta.text
14520        );
14521        assert!(!src_delta.text.contains("faceOf(extrude001, face ="));
14522
14523        ctx.close().await;
14524    }
14525
14526    #[test]
14527    fn test_enclosing_variable_fallback_skips_nested_sketch_items() {
14528        let source = "\
14529sketch001 = sketch(on = XY) {
14530  line(start = [0, 0], end = [1, 0])
14531}
14532part = subtract(boxSolid, tools = [cutSolid])
14533  |> appearance(color = \"#8f96a3\")
14534";
14535        let ast = Program::parse(source).unwrap().0.unwrap().ast;
14536        let line_start = source.find("line").unwrap();
14537        let line_end = line_start + "line(start = [0, 0], end = [1, 0])".len();
14538        let line_ref = SourceRef::Simple {
14539            range: [line_start, line_end, 0].into(),
14540            node_path: None,
14541        };
14542        assert_eq!(variable_name_containing_source_ref(&ast, &line_ref), None);
14543
14544        let subtract_start = source.find("subtract").unwrap();
14545        let subtract_end = subtract_start + "subtract(boxSolid, tools = [cutSolid])".len();
14546        let subtract_ref = SourceRef::Simple {
14547            range: [subtract_start, subtract_end, 0].into(),
14548            node_path: None,
14549        };
14550        assert_eq!(
14551            variable_name_containing_source_ref(&ast, &subtract_ref),
14552            Some("part".to_owned())
14553        );
14554    }
14555
14556    #[tokio::test(flavor = "multi_thread")]
14557    async fn test_sketch_on_subtracted_sweep_cap_uses_composite_solid() {
14558        clear_mem_cache().await;
14559        let source = "\
14560boxSolid = startSketchOn(XY)
14561  |> startProfile(at = [0, 0])
14562  |> line(end = [4, 0], tag = $bottomEdge)
14563  |> line(end = [0, 4])
14564  |> line(end = [-4, 0])
14565  |> close()
14566  |> extrude(length = 10)
14567cutSolid = startSketchOn(XY)
14568  |> startProfile(at = [1, 1])
14569  |> line(end = [1, 0])
14570  |> line(end = [0, 1])
14571  |> line(end = [-1, 0])
14572  |> close()
14573  |> extrude(length = 10)
14574part = subtract(boxSolid, tools = [cutSolid])
14575  |> appearance(color = \"#8f96a3\", roughness = 55, metalness = 8)
14576";
14577        let program = Program::parse(source).unwrap().0.unwrap();
14578        let mut frontend = FrontendState::new();
14579        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14580        match frontend.hack_set_program(&ctx, program).await.unwrap() {
14581            SetProgramOutcome::Success { .. } => {}
14582            SetProgramOutcome::ExecFailure { error } => panic!("KCL fixture failed to execute: {error:?}"),
14583        }
14584
14585        let sweep_call_start = source.find("extrude").unwrap();
14586        let sweep_call_end = sweep_call_start + "extrude(length = 10)".len();
14587        let part_call_start = source.find("subtract").unwrap();
14588        let part_call_end = part_call_start + "subtract(boxSolid, tools = [cutSolid])".len();
14589        let sweep_range = [sweep_call_start, sweep_call_end, 0].into();
14590        let composite_range = [part_call_start, part_call_end, 0].into();
14591
14592        let cap_object = frontend
14593            .scene_graph
14594            .objects
14595            .iter()
14596            .find(|object| {
14597                matches!(
14598                    &object.kind,
14599                    ObjectKind::Cap(crate::frontend::api::Cap {
14600                        kind: crate::frontend::api::CapKind::End,
14601                        source,
14602                        ..
14603                    }) if source.solid.range == composite_range && source.sweep.range == sweep_range
14604                )
14605            })
14606            .expect("expected end cap object to trace through subtract and original extrude");
14607
14608        let mut ast = frontend.program.ast.clone();
14609        let cap_expr = sketch_on_ast_expr(
14610            &mut ast,
14611            &frontend.scene_graph,
14612            &frontend.solid_references,
14613            &Plane::Object(cap_object.id),
14614        )
14615        .unwrap();
14616        let cap_face_decl = ast::VariableDeclaration::new(
14617            ast::VariableDeclarator::new("capFace", cap_expr.clone()),
14618            ast::ItemVisibility::Default,
14619            ast::VariableKind::Const,
14620        );
14621        ast.body
14622            .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
14623                cap_face_decl,
14624            ))));
14625        let generated_source = source_from_ast(&ast);
14626
14627        assert!(generated_source.contains("capFace = faceOf(part, face = END)"));
14628        assert!(!generated_source.contains("faceOf(boxSolid"));
14629        let ast::Expr::CallExpressionKw(call) = cap_expr else {
14630            panic!("expected faceOf call");
14631        };
14632        assert_eq!(call.callee.name.name, "faceOf");
14633        let ast::Expr::Name(solid_name) = call.unlabeled.as_ref().unwrap() else {
14634            panic!("expected solid name");
14635        };
14636        assert_eq!(solid_name.name.name, "part");
14637        let ast::Expr::Name(face_name) = &call.arguments[0].arg else {
14638            panic!("expected face name");
14639        };
14640        assert_eq!(face_name.name.name, "END");
14641
14642        ctx.close().await;
14643    }
14644
14645    #[tokio::test(flavor = "multi_thread")]
14646    async fn test_sketch_on_plane_incremental() {
14647        let initial_source = "\
14648len = 2mm
14649cube = startSketchOn(XY)
14650  |> startProfile(at = [0, 0])
14651  |> line(end = [len, 0], tag = $side)
14652  |> line(end = [0, len])
14653  |> line(end = [-len, 0])
14654  |> line(end = [0, -len])
14655  |> close()
14656  |> extrude(length = len)
14657
14658plane = planeOf(cube, face = side)
14659";
14660
14661        let program = Program::parse(initial_source).unwrap().0.unwrap();
14662
14663        let mut frontend = FrontendState::new();
14664
14665        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14666        let mock_ctx = ExecutorContext::new_mock(None).await;
14667        let version = Version(0);
14668
14669        frontend.hack_set_program(&ctx, program).await.unwrap();
14670        // Find the last plane since the first plane is the XY plane.
14671        let plane_object = frontend
14672            .scene_graph
14673            .objects
14674            .iter()
14675            .rev()
14676            .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
14677            .unwrap();
14678        let plane_id = plane_object.id;
14679
14680        let sketch_args = SketchCtor {
14681            on: Plane::Object(plane_id),
14682        };
14683        let (src_delta, scene_delta, sketch_id) = frontend
14684            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14685            .await
14686            .unwrap();
14687        insta::assert_snapshot!("test_sketch_on_plane_incremental", src_delta.text.as_str());
14688        assert_eq!(sketch_id, ObjectId(2));
14689        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14690        let sketch_object = &scene_delta.new_graph.objects[2];
14691        assert_eq!(sketch_object.id, ObjectId(2));
14692        assert_eq!(
14693            sketch_object.kind,
14694            ObjectKind::Sketch(Sketch {
14695                args: SketchCtor {
14696                    on: Plane::Object(plane_id),
14697                },
14698                plane: plane_id,
14699                segments: vec![],
14700                constraints: vec![],
14701            })
14702        );
14703        assert_eq!(scene_delta.new_graph.objects.len(), 9);
14704
14705        let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
14706        assert_eq!(plane_object.id, plane_id);
14707        assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
14708
14709        ctx.close().await;
14710        mock_ctx.close().await;
14711    }
14712
14713    #[tokio::test(flavor = "multi_thread")]
14714    async fn test_new_sketch_uses_unique_variable_name() {
14715        let initial_source = "\
14716sketch1 = sketch(on = XY) {
14717}
14718";
14719
14720        let program = Program::parse(initial_source).unwrap().0.unwrap();
14721
14722        let mut frontend = FrontendState::new();
14723        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14724        let version = Version(0);
14725
14726        frontend.hack_set_program(&ctx, program).await.unwrap();
14727
14728        let sketch_args = SketchCtor {
14729            on: Plane::Default(PlaneName::Yz),
14730        };
14731        let (src_delta, _, _) = frontend
14732            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14733            .await
14734            .unwrap();
14735
14736        insta::assert_snapshot!("test_new_sketch_uses_unique_variable_name", src_delta.text.as_str());
14737
14738        ctx.close().await;
14739    }
14740
14741    #[tokio::test(flavor = "multi_thread")]
14742    async fn test_new_sketch_twice_using_same_plane() {
14743        let initial_source = "\
14744sketch1 = sketch(on = XY) {
14745}
14746";
14747
14748        let program = Program::parse(initial_source).unwrap().0.unwrap();
14749
14750        let mut frontend = FrontendState::new();
14751        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14752        let version = Version(0);
14753
14754        frontend.hack_set_program(&ctx, program).await.unwrap();
14755
14756        let sketch_args = SketchCtor {
14757            on: Plane::Default(PlaneName::Xy),
14758        };
14759        let (src_delta, _, _) = frontend
14760            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14761            .await
14762            .unwrap();
14763
14764        insta::assert_snapshot!("test_new_sketch_twice_using_same_plane", src_delta.text.as_str());
14765
14766        ctx.close().await;
14767    }
14768
14769    #[tokio::test(flavor = "multi_thread")]
14770    async fn test_sketch_mode_reuses_cached_on_expression() {
14771        let initial_source = "\
14772width = 2mm
14773sketch(on = offsetPlane(XY, offset = width)) {
14774  line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
14775  distance([line1.start, line1.end]) == width
14776}
14777";
14778        let program = Program::parse(initial_source).unwrap().0.unwrap();
14779
14780        let mut frontend = FrontendState::new();
14781        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14782        let mock_ctx = ExecutorContext::new_mock(None).await;
14783        let version = Version(0);
14784        let project_id = ProjectId(0);
14785        let file_id = FileId(0);
14786
14787        frontend.hack_set_program(&ctx, program).await.unwrap();
14788        let initial_object_count = frontend.scene_graph.objects.len();
14789        let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14790            .expect("Expected sketch object to exist")
14791            .id;
14792
14793        // Entering sketch mode should reuse cached `on` expression state
14794        // (offsetPlane result), not fail or create extra on-surface objects.
14795        let scene_delta = frontend
14796            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14797            .await
14798            .unwrap();
14799        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14800
14801        // A follow-up sketch-mode execution should keep the same stable object
14802        // graph shape as well.
14803        let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
14804        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14805
14806        ctx.close().await;
14807        mock_ctx.close().await;
14808    }
14809
14810    #[tokio::test(flavor = "multi_thread")]
14811    async fn test_edit_sketch_nested_in_pipe() {
14812        clear_mem_cache().await;
14813        let source = r#"
14814profile = sketch(on = XY) {
14815  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
14816}
14817  |> translate(x = 2mm)
14818"#;
14819        let program = Program::parse_no_errs(source).unwrap();
14820        let mut frontend = FrontendState::new();
14821        let mock_ctx = ExecutorContext::new_mock(None).await;
14822        let version = Version(0);
14823
14824        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
14825        let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14826            .expect("Expected piped sketch object")
14827            .id;
14828
14829        let scene_delta = frontend
14830            .edit_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_id)
14831            .await
14832            .unwrap();
14833        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch_id));
14834        assert!(
14835            scene_delta
14836                .new_graph
14837                .objects
14838                .iter()
14839                .any(|object| matches!(&object.kind, ObjectKind::Segment { .. })),
14840            "Expected the piped sketch's segments to be present in sketch mode"
14841        );
14842
14843        clear_mem_cache().await;
14844        mock_ctx.close().await;
14845    }
14846
14847    #[tokio::test(flavor = "multi_thread")]
14848    async fn test_issue_9409_edit_sketch_nested_in_if_with_var_feedback() {
14849        clear_mem_cache().await;
14850        let source = r#"
14851useFirstProfile = true
14852
14853profile = if useFirstProfile {
14854  sketch(on = XY) {
14855    line1 = line(start = [0mm, 0mm], end = [var 20mm, var 10mm])
14856  }
14857} else {
14858  sketch(on = XY) {
14859    line2 = line(start = [0mm, 0mm], end = [var 10mm, var 20mm])
14860  }
14861}
14862"#;
14863        let program = Program::parse_no_errs(source).unwrap();
14864        let mut frontend = FrontendState::new();
14865        let mock_ctx = ExecutorContext::new_mock(None).await;
14866        let version = Version(0);
14867
14868        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
14869        let sketch_object =
14870            find_first_sketch_object(&frontend.scene_graph).expect("Expected active branch's sketch object");
14871        let sketch_id = sketch_object.id;
14872        let sketch = expect_sketch(sketch_object);
14873        let line_end_id = *sketch
14874            .segments
14875            .get(1)
14876            .expect("Expected the active branch's line end point");
14877
14878        let scene_delta = frontend
14879            .edit_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_id)
14880            .await
14881            .unwrap();
14882        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch_id));
14883
14884        let segments = vec![ExistingSegmentCtor {
14885            id: line_end_id,
14886            ctor: SegmentCtor::Point(PointCtor {
14887                position: Point2d {
14888                    x: Expr::Var(Number {
14889                        value: 30.0,
14890                        units: NumericSuffix::Mm,
14891                    }),
14892                    y: Expr::Var(Number {
14893                        value: 15.0,
14894                        units: NumericSuffix::Mm,
14895                    }),
14896                },
14897            }),
14898        }];
14899        let (source_delta, _) = frontend
14900            .edit_segments(&mock_ctx, version, sketch_id, segments)
14901            .await
14902            .unwrap();
14903        assert!(
14904            source_delta
14905                .text
14906                .contains("line1 = line(start = [0mm, 0mm], end = [var 30mm, var 15mm])"),
14907            "Expected the active branch's dragged variables to be updated:\n{}",
14908            source_delta.text
14909        );
14910        assert!(
14911            source_delta
14912                .text
14913                .contains("line2 = line(start = [0mm, 0mm], end = [var 10mm, var 20mm])"),
14914            "Expected the inactive branch to remain unchanged:\n{}",
14915            source_delta.text
14916        );
14917
14918        clear_mem_cache().await;
14919        mock_ctx.close().await;
14920    }
14921
14922    #[tokio::test(flavor = "multi_thread")]
14923    async fn test_multiple_sketch_blocks() {
14924        let initial_source = "\
14925// Cube that requires the engine.
14926width = 2
14927sketch001 = startSketchOn(XY)
14928profile001 = startProfile(sketch001, at = [0, 0])
14929  |> yLine(length = width, tag = $seg1)
14930  |> xLine(length = width)
14931  |> yLine(length = -width)
14932  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14933  |> close()
14934extrude001 = extrude(profile001, length = width)
14935
14936// Get a value that requires the engine.
14937x = segLen(seg1)
14938
14939// Triangle with side length 2*x.
14940sketch(on = XY) {
14941  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14942  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
14943  coincident([line1.end, line2.start])
14944  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
14945  coincident([line2.end, line3.start])
14946  coincident([line3.end, line1.start])
14947  equalLength([line3, line1])
14948  equalLength([line1, line2])
14949  distance([line1.start, line1.end]) == 2*x
14950}
14951
14952// Line segment with length x.
14953sketch2 = sketch(on = XY) {
14954  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14955  distance([line1.start, line1.end]) == x
14956}
14957";
14958
14959        let program = Program::parse(initial_source).unwrap().0.unwrap();
14960
14961        let mut frontend = FrontendState::new();
14962
14963        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14964        let mock_ctx = ExecutorContext::new_mock(None).await;
14965        let version = Version(0);
14966        let project_id = ProjectId(0);
14967        let file_id = FileId(0);
14968
14969        frontend.hack_set_program(&ctx, program).await.unwrap();
14970        let sketch_objects = frontend
14971            .scene_graph
14972            .objects
14973            .iter()
14974            .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
14975            .collect::<Vec<_>>();
14976        let sketch1_id = sketch_objects.first().unwrap().id;
14977        let sketch2_id = sketch_objects.get(1).unwrap().id;
14978        // First point in sketch1.
14979        let point1_id = ObjectId(sketch1_id.0 + 1);
14980        // First point in sketch2.
14981        let point2_id = ObjectId(sketch2_id.0 + 1);
14982
14983        // Edit the first sketch. Objects before the sketch block should be
14984        // present from execution cache so that we can sketch on prior planes,
14985        // for example. Objects after the first sketch block should not be
14986        // present since those statements are skipped in sketch mode.
14987        //
14988        // - startSketchOn(XY) Plane 1
14989        // - sketch on=XY Plane 1
14990        // - Sketch block 16
14991        let scene_delta = frontend
14992            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
14993            .await
14994            .unwrap();
14995        assert_eq!(
14996            scene_delta.new_graph.objects.len(),
14997            18,
14998            "{:#?}",
14999            scene_delta.new_graph.objects
15000        );
15001
15002        // Edit a point in the first sketch.
15003        let point_ctor = PointCtor {
15004            position: Point2d {
15005                x: Expr::Var(Number {
15006                    value: 1.0,
15007                    units: NumericSuffix::Mm,
15008                }),
15009                y: Expr::Var(Number {
15010                    value: 2.0,
15011                    units: NumericSuffix::Mm,
15012                }),
15013            },
15014        };
15015        let segments = vec![ExistingSegmentCtor {
15016            id: point1_id,
15017            ctor: SegmentCtor::Point(point_ctor),
15018        }];
15019        let (src_delta, _) = frontend
15020            .edit_segments(&mock_ctx, version, sketch1_id, segments)
15021            .await
15022            .unwrap();
15023        // Only the first sketch block changes.
15024        insta::assert_snapshot!("test_multiple_sketch_blocks_1", src_delta.text.as_str());
15025        let edited_sketch1_source = src_delta.text.clone();
15026
15027        // Execute mock to simulate drag end.
15028        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
15029        assert_eq!(src_delta.text, edited_sketch1_source);
15030        // Exit sketch. Objects from the entire program should be present.
15031        //
15032        // - startSketchOn(XY) Plane 1
15033        // - sketch on=XY Plane 1
15034        // - Sketch block 16
15035        // - sketch on=XY cached
15036        // - Sketch block 5
15037        let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
15038        assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
15039
15040        // Edit the second sketch.
15041        //
15042        // - startSketchOn(XY) Plane 1
15043        // - sketch on=XY Plane 1
15044        // - Sketch block 16
15045        // - sketch on=XY cached
15046        // - Sketch block 5
15047        let scene_delta = frontend
15048            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
15049            .await
15050            .unwrap();
15051        assert_eq!(
15052            scene_delta.new_graph.objects.len(),
15053            24,
15054            "{:#?}",
15055            scene_delta.new_graph.objects
15056        );
15057
15058        // Edit a point in the second sketch.
15059        let point_ctor = PointCtor {
15060            position: Point2d {
15061                x: Expr::Var(Number {
15062                    value: 3.0,
15063                    units: NumericSuffix::Mm,
15064                }),
15065                y: Expr::Var(Number {
15066                    value: 4.0,
15067                    units: NumericSuffix::Mm,
15068                }),
15069            },
15070        };
15071        let segments = vec![ExistingSegmentCtor {
15072            id: point2_id,
15073            ctor: SegmentCtor::Point(point_ctor),
15074        }];
15075        let (src_delta, _) = frontend
15076            .edit_segments(&mock_ctx, version, sketch2_id, segments)
15077            .await
15078            .unwrap();
15079        // Only the second sketch block changes.
15080        insta::assert_snapshot!("test_multiple_sketch_blocks_2", src_delta.text.as_str());
15081        let edited_sketch2_source = src_delta.text.clone();
15082
15083        // Execute mock to simulate drag end.
15084        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
15085        assert_eq!(src_delta.text, edited_sketch2_source);
15086
15087        ctx.close().await;
15088        mock_ctx.close().await;
15089    }
15090
15091    #[tokio::test(flavor = "multi_thread")]
15092    async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
15093        clear_mem_cache().await;
15094
15095        let source = r#"sketch001 = sketch(on = XZ) {
15096  circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
15097}
15098sketch002 = sketch(on = XY) {
15099  line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
15100  line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
15101  line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
15102  line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
15103  coincident([line1.end, line2.start])
15104  coincident([line2.end, line3.start])
15105  coincident([line3.end, line4.start])
15106  coincident([line4.end, line1.start])
15107  parallel([line2, line4])
15108  parallel([line3, line1])
15109  perpendicular([line1, line2])
15110  horizontal(line3)
15111  coincident([line1.start, ORIGIN])
15112}
15113"#;
15114
15115        let program = Program::parse(source).unwrap().0.unwrap();
15116        let mut frontend = FrontendState::new();
15117        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
15118        let mock_ctx = ExecutorContext::new_mock(None).await;
15119        let version = Version(0);
15120        let project_id = ProjectId(0);
15121        let file_id = FileId(0);
15122
15123        frontend.hack_set_program(&ctx, program).await.unwrap();
15124        let sketch_objects = frontend
15125            .scene_graph
15126            .objects
15127            .iter()
15128            .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
15129            .collect::<Vec<_>>();
15130        assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
15131
15132        let sketch1_id = sketch_objects[0].id;
15133        let sketch2_id = sketch_objects[1].id;
15134
15135        frontend
15136            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
15137            .await
15138            .unwrap();
15139        frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
15140
15141        let scene_delta = frontend
15142            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
15143            .await
15144            .unwrap();
15145        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
15146
15147        clear_mem_cache().await;
15148        ctx.close().await;
15149        mock_ctx.close().await;
15150    }
15151
15152    // Regression tests: operations on source code with extra whitespace/newlines.
15153    // These test that NodePath-based lookups work correctly when source ranges
15154    // are shifted by extra whitespace that wouldn't be present after formatting.
15155
15156    #[tokio::test(flavor = "multi_thread")]
15157    async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
15158        // Extra newlines after @settings line - this shifts all source ranges.
15159        let initial_source = "@settings(defaultLengthUnit = mm)
15160
15161sketch001 = sketch(on = XY) {
15162  point(at = [1in, 2in])
15163}
15164";
15165
15166        let program = Program::parse(initial_source).unwrap().0.unwrap();
15167        let mut frontend = FrontendState::new();
15168
15169        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15170        let mock_ctx = ExecutorContext::new_mock(None).await;
15171        let version = Version(0);
15172        let project_id = ProjectId(0);
15173        let file_id = FileId(0);
15174
15175        frontend.hack_set_program(&ctx, program).await.unwrap();
15176        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15177        let sketch_id = sketch_object.id;
15178
15179        // Edit sketch should succeed despite extra newlines.
15180        frontend
15181            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15182            .await
15183            .unwrap();
15184
15185        // Add a new point to the sketch.
15186        let point_ctor = PointCtor {
15187            position: Point2d {
15188                x: Expr::Number(Number {
15189                    value: 5.0,
15190                    units: NumericSuffix::Mm,
15191                }),
15192                y: Expr::Number(Number {
15193                    value: 6.0,
15194                    units: NumericSuffix::Mm,
15195                }),
15196            },
15197        };
15198        let segment = SegmentCtor::Point(point_ctor);
15199        let (src_delta, scene_delta) = frontend
15200            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15201            .await
15202            .unwrap();
15203        // After adding a point, the source should be reformatted with standard whitespace.
15204        assert!(
15205            src_delta.text.contains("point(at = [5mm, 6mm])"),
15206            "Expected new point in source, got: {}",
15207            src_delta.text
15208        );
15209        assert!(!scene_delta.new_objects.is_empty());
15210
15211        ctx.close().await;
15212        mock_ctx.close().await;
15213    }
15214
15215    #[tokio::test(flavor = "multi_thread")]
15216    async fn test_ensure_control_point_spline_experimental_features_adds_allow_setting() {
15217        let initial_program = Program::parse("s = sketch(on = XY) {}\n").unwrap().0.unwrap();
15218
15219        let updated_program = ensure_control_point_spline_experimental_features(&initial_program).unwrap();
15220        let meta_settings = updated_program.meta_settings().unwrap().unwrap();
15221
15222        assert_eq!(meta_settings.experimental_features, WarningLevel::Allow);
15223        assert!(
15224            source_from_ast(&updated_program.ast).contains("@settings(experimentalFeatures = allow)"),
15225            "Expected experimental settings to be added to source"
15226        );
15227    }
15228
15229    #[tokio::test(flavor = "multi_thread")]
15230    async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
15231        // Extra newlines after @settings, with an empty sketch block.
15232        let initial_source = "@settings(defaultLengthUnit = mm)
15233
15234s = sketch(on = XY) {}
15235";
15236
15237        let program = Program::parse(initial_source).unwrap().0.unwrap();
15238        let mut frontend = FrontendState::new();
15239
15240        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15241        let mock_ctx = ExecutorContext::new_mock(None).await;
15242        let version = Version(0);
15243
15244        frontend.hack_set_program(&ctx, program).await.unwrap();
15245        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15246        let sketch_id = sketch_object.id;
15247
15248        let line_ctor = LineCtor {
15249            start: Point2d {
15250                x: Expr::Number(Number {
15251                    value: 0.0,
15252                    units: NumericSuffix::Mm,
15253                }),
15254                y: Expr::Number(Number {
15255                    value: 0.0,
15256                    units: NumericSuffix::Mm,
15257                }),
15258            },
15259            end: Point2d {
15260                x: Expr::Number(Number {
15261                    value: 10.0,
15262                    units: NumericSuffix::Mm,
15263                }),
15264                y: Expr::Number(Number {
15265                    value: 10.0,
15266                    units: NumericSuffix::Mm,
15267                }),
15268            },
15269            construction: None,
15270        };
15271        let segment = SegmentCtor::Line(line_ctor);
15272        let (src_delta, scene_delta) = frontend
15273            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15274            .await
15275            .unwrap();
15276        assert!(
15277            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15278            "Expected line in source, got: {}",
15279            src_delta.text
15280        );
15281        // Line creates start point, end point, and line segment.
15282        assert_eq!(scene_delta.new_objects.len(), 3);
15283
15284        ctx.close().await;
15285        mock_ctx.close().await;
15286    }
15287
15288    #[tokio::test(flavor = "multi_thread")]
15289    async fn test_extra_newlines_between_operations_edit_line() {
15290        // Extra newlines between @settings and sketch, and inside the sketch block.
15291        let initial_source = "@settings(defaultLengthUnit = mm)
15292
15293sketch001 = sketch(on = XY) {
15294
15295  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
15296
15297}
15298";
15299
15300        let program = Program::parse(initial_source).unwrap().0.unwrap();
15301        let mut frontend = FrontendState::new();
15302
15303        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15304        let mock_ctx = ExecutorContext::new_mock(None).await;
15305        let version = Version(0);
15306        let project_id = ProjectId(0);
15307        let file_id = FileId(0);
15308
15309        frontend.hack_set_program(&ctx, program).await.unwrap();
15310        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15311        let sketch_id = sketch_object.id;
15312        let sketch = expect_sketch(sketch_object);
15313
15314        // Extract segment IDs before edit_sketch borrows frontend mutably.
15315        let line_id = sketch
15316            .segments
15317            .iter()
15318            .copied()
15319            .find(|seg_id| {
15320                matches!(
15321                    &frontend.scene_graph.objects[seg_id.0].kind,
15322                    ObjectKind::Segment {
15323                        segment: Segment::Line(_)
15324                    }
15325                )
15326            })
15327            .expect("Expected a line segment in sketch");
15328
15329        // Enter sketch edit mode.
15330        frontend
15331            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15332            .await
15333            .unwrap();
15334
15335        // Edit the line.
15336        let line_ctor = LineCtor {
15337            start: Point2d {
15338                x: Expr::Var(Number {
15339                    value: 1.0,
15340                    units: NumericSuffix::Mm,
15341                }),
15342                y: Expr::Var(Number {
15343                    value: 2.0,
15344                    units: NumericSuffix::Mm,
15345                }),
15346            },
15347            end: Point2d {
15348                x: Expr::Var(Number {
15349                    value: 13.0,
15350                    units: NumericSuffix::Mm,
15351                }),
15352                y: Expr::Var(Number {
15353                    value: 14.0,
15354                    units: NumericSuffix::Mm,
15355                }),
15356            },
15357            construction: None,
15358        };
15359        let segments = vec![ExistingSegmentCtor {
15360            id: line_id,
15361            ctor: SegmentCtor::Line(line_ctor),
15362        }];
15363        let (src_delta, _scene_delta) = frontend
15364            .edit_segments(&mock_ctx, version, sketch_id, segments)
15365            .await
15366            .unwrap();
15367        assert!(
15368            src_delta
15369                .text
15370                .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
15371            "Expected edited line in source, got: {}",
15372            src_delta.text
15373        );
15374
15375        ctx.close().await;
15376        mock_ctx.close().await;
15377    }
15378
15379    #[tokio::test(flavor = "multi_thread")]
15380    async fn test_extra_newlines_delete_segment() {
15381        // Extra whitespace before and after the sketch block.
15382        let initial_source = "@settings(defaultLengthUnit = mm)
15383
15384sketch001 = sketch(on = XY) {
15385  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
15386}
15387";
15388
15389        let program = Program::parse(initial_source).unwrap().0.unwrap();
15390        let mut frontend = FrontendState::new();
15391
15392        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15393        let mock_ctx = ExecutorContext::new_mock(None).await;
15394        let version = Version(0);
15395
15396        frontend.hack_set_program(&ctx, program).await.unwrap();
15397        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15398        let sketch_id = sketch_object.id;
15399        let sketch = expect_sketch(sketch_object);
15400
15401        // The sketch should have 3 segments: start point, center point, and the circle.
15402        assert_eq!(sketch.segments.len(), 3);
15403        let circle_id = sketch.segments[2];
15404
15405        // Delete the circle despite extra newlines in original source.
15406        let (src_delta, scene_delta) = frontend
15407            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
15408            .await
15409            .unwrap();
15410        assert!(
15411            src_delta.text.contains("sketch(on = XY) {"),
15412            "Expected sketch block in source, got: {}",
15413            src_delta.text
15414        );
15415        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
15416        let new_sketch = expect_sketch(new_sketch_object);
15417        assert_eq!(new_sketch.segments.len(), 0);
15418
15419        ctx.close().await;
15420        mock_ctx.close().await;
15421    }
15422
15423    #[tokio::test(flavor = "multi_thread")]
15424    async fn test_unformatted_source_add_arc() {
15425        // Source with inconsistent whitespace - tabs, extra spaces, multiple blank lines.
15426        let initial_source = "@settings(defaultLengthUnit = mm)
15427
15428sketch001 = sketch(on = XY) {
15429}
15430";
15431
15432        let program = Program::parse(initial_source).unwrap().0.unwrap();
15433        let mut frontend = FrontendState::new();
15434
15435        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15436        let mock_ctx = ExecutorContext::new_mock(None).await;
15437        let version = Version(0);
15438
15439        frontend.hack_set_program(&ctx, program).await.unwrap();
15440        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15441        let sketch_id = sketch_object.id;
15442
15443        let arc_ctor = ArcCtor {
15444            start: Point2d {
15445                x: Expr::Var(Number {
15446                    value: 5.0,
15447                    units: NumericSuffix::Mm,
15448                }),
15449                y: Expr::Var(Number {
15450                    value: 0.0,
15451                    units: NumericSuffix::Mm,
15452                }),
15453            },
15454            end: Point2d {
15455                x: Expr::Var(Number {
15456                    value: 0.0,
15457                    units: NumericSuffix::Mm,
15458                }),
15459                y: Expr::Var(Number {
15460                    value: 5.0,
15461                    units: NumericSuffix::Mm,
15462                }),
15463            },
15464            center: Point2d {
15465                x: Expr::Var(Number {
15466                    value: 0.0,
15467                    units: NumericSuffix::Mm,
15468                }),
15469                y: Expr::Var(Number {
15470                    value: 0.0,
15471                    units: NumericSuffix::Mm,
15472                }),
15473            },
15474            direction: None,
15475            construction: None,
15476        };
15477        let segment = SegmentCtor::Arc(arc_ctor);
15478        let (src_delta, scene_delta) = frontend
15479            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15480            .await
15481            .unwrap();
15482        assert!(
15483            src_delta
15484                .text
15485                .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
15486            "Expected arc in source, got: {}",
15487            src_delta.text
15488        );
15489        assert!(!scene_delta.new_objects.is_empty());
15490
15491        ctx.close().await;
15492        mock_ctx.close().await;
15493    }
15494
15495    #[tokio::test(flavor = "multi_thread")]
15496    async fn test_arc_direction_flows_to_source() {
15497        let initial_source = "@settings(defaultLengthUnit = mm)
15498
15499sketch001 = sketch(on = XY) {
15500}
15501";
15502
15503        let program = Program::parse(initial_source).unwrap().0.unwrap();
15504        let mut frontend = FrontendState::new();
15505
15506        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15507        let mock_ctx = ExecutorContext::new_mock(None).await;
15508        let version = Version(0);
15509
15510        frontend.hack_set_program(&ctx, program).await.unwrap();
15511        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15512        let sketch_id = sketch_object.id;
15513
15514        let point = |x: f64, y: f64| Point2d {
15515            x: Expr::Var(Number {
15516                value: x,
15517                units: NumericSuffix::Mm,
15518            }),
15519            y: Expr::Var(Number {
15520                value: y,
15521                units: NumericSuffix::Mm,
15522            }),
15523        };
15524
15525        // Adding a clockwise arc writes its direction to the source.
15526        let arc_ctor = ArcCtor {
15527            start: point(5.0, 0.0),
15528            end: point(0.0, 5.0),
15529            center: point(0.0, 0.0),
15530            direction: Some(ArcDirection::Cw),
15531            construction: None,
15532        };
15533        let (src_delta, scene_delta) = frontend
15534            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
15535            .await
15536            .unwrap();
15537        assert!(
15538            src_delta.text.contains("direction = CW"),
15539            "Expected direction = CW in source, got: {}",
15540            src_delta.text
15541        );
15542        // The new objects are the end points, the center, and then the arc.
15543        let arc_id = *scene_delta.new_objects.last().unwrap();
15544
15545        // Editing the arc's points preserves the clockwise direction. The
15546        // edited points keep the same distance to the center so that the
15547        // solver doesn't need to move anything.
15548        let edited_ctor = ArcCtor {
15549            start: point(0.0, -5.0),
15550            end: point(0.0, 5.0),
15551            center: point(0.0, 0.0),
15552            direction: Some(ArcDirection::Cw),
15553            construction: None,
15554        };
15555        let (src_delta, _scene_delta) = frontend
15556            .edit_segments(
15557                &mock_ctx,
15558                version,
15559                sketch_id,
15560                vec![ExistingSegmentCtor {
15561                    id: arc_id,
15562                    ctor: SegmentCtor::Arc(edited_ctor),
15563                }],
15564            )
15565            .await
15566            .unwrap();
15567        assert!(
15568            src_delta.text.contains("start = [var 0mm, var -5mm]"),
15569            "Expected edited start point in source, got: {}",
15570            src_delta.text
15571        );
15572        assert!(
15573            src_delta.text.contains("direction = CW"),
15574            "Expected direction = CW to be preserved in source, got: {}",
15575            src_delta.text
15576        );
15577
15578        // Editing the arc back to counterclockwise removes the direction
15579        // argument since counterclockwise is the default.
15580        let edited_ctor = ArcCtor {
15581            start: point(0.0, -5.0),
15582            end: point(0.0, 5.0),
15583            center: point(0.0, 0.0),
15584            direction: Some(ArcDirection::Ccw),
15585            construction: None,
15586        };
15587        let (src_delta, _scene_delta) = frontend
15588            .edit_segments(
15589                &mock_ctx,
15590                version,
15591                sketch_id,
15592                vec![ExistingSegmentCtor {
15593                    id: arc_id,
15594                    ctor: SegmentCtor::Arc(edited_ctor),
15595                }],
15596            )
15597            .await
15598            .unwrap();
15599        assert!(
15600            !src_delta.text.contains("direction"),
15601            "Expected direction argument to be removed from source, got: {}",
15602            src_delta.text
15603        );
15604
15605        ctx.close().await;
15606        mock_ctx.close().await;
15607    }
15608
15609    #[tokio::test(flavor = "multi_thread")]
15610    async fn test_extra_newlines_add_circle() {
15611        // Extra blank lines between settings and sketch.
15612        let initial_source = "@settings(defaultLengthUnit = mm)
15613
15614sketch001 = sketch(on = XY) {
15615}
15616";
15617
15618        let program = Program::parse(initial_source).unwrap().0.unwrap();
15619        let mut frontend = FrontendState::new();
15620
15621        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15622        let mock_ctx = ExecutorContext::new_mock(None).await;
15623        let version = Version(0);
15624
15625        frontend.hack_set_program(&ctx, program).await.unwrap();
15626        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15627        let sketch_id = sketch_object.id;
15628
15629        let circle_ctor = CircleCtor {
15630            start: Point2d {
15631                x: Expr::Var(Number {
15632                    value: 5.0,
15633                    units: NumericSuffix::Mm,
15634                }),
15635                y: Expr::Var(Number {
15636                    value: 0.0,
15637                    units: NumericSuffix::Mm,
15638                }),
15639            },
15640            center: Point2d {
15641                x: Expr::Var(Number {
15642                    value: 0.0,
15643                    units: NumericSuffix::Mm,
15644                }),
15645                y: Expr::Var(Number {
15646                    value: 0.0,
15647                    units: NumericSuffix::Mm,
15648                }),
15649            },
15650            construction: None,
15651        };
15652        let segment = SegmentCtor::Circle(circle_ctor);
15653        let (src_delta, scene_delta) = frontend
15654            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15655            .await
15656            .unwrap();
15657        assert!(
15658            src_delta
15659                .text
15660                .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
15661            "Expected circle in source, got: {}",
15662            src_delta.text
15663        );
15664        assert!(!scene_delta.new_objects.is_empty());
15665
15666        ctx.close().await;
15667        mock_ctx.close().await;
15668    }
15669
15670    #[tokio::test(flavor = "multi_thread")]
15671    async fn test_extra_newlines_add_constraint() {
15672        // Extra newlines with a sketch containing two lines - add a coincident constraint.
15673        let initial_source = "@settings(defaultLengthUnit = mm)
15674
15675sketch001 = sketch(on = XY) {
15676  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
15677  line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
15678}
15679";
15680
15681        let program = Program::parse(initial_source).unwrap().0.unwrap();
15682        let mut frontend = FrontendState::new();
15683
15684        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15685        let mock_ctx = ExecutorContext::new_mock(None).await;
15686        let version = Version(0);
15687        let project_id = ProjectId(0);
15688        let file_id = FileId(0);
15689
15690        frontend.hack_set_program(&ctx, program).await.unwrap();
15691        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15692        let sketch_id = sketch_object.id;
15693        let sketch = expect_sketch(sketch_object);
15694
15695        // Extract segment data before edit_sketch borrows frontend mutably.
15696        let line_ids: Vec<ObjectId> = sketch
15697            .segments
15698            .iter()
15699            .copied()
15700            .filter(|seg_id| {
15701                matches!(
15702                    &frontend.scene_graph.objects[seg_id.0].kind,
15703                    ObjectKind::Segment {
15704                        segment: Segment::Line(_)
15705                    }
15706                )
15707            })
15708            .collect();
15709        assert_eq!(line_ids.len(), 2, "Expected two line segments");
15710
15711        let line1 = &frontend.scene_graph.objects[line_ids[0].0];
15712        let ObjectKind::Segment {
15713            segment: Segment::Line(line1_data),
15714        } = &line1.kind
15715        else {
15716            panic!("Expected line");
15717        };
15718        let line2 = &frontend.scene_graph.objects[line_ids[1].0];
15719        let ObjectKind::Segment {
15720            segment: Segment::Line(line2_data),
15721        } = &line2.kind
15722        else {
15723            panic!("Expected line");
15724        };
15725
15726        // Build constraint before entering sketch mode.
15727        let constraint = Constraint::Coincident(Coincident {
15728            segments: vec![line1_data.end.into(), line2_data.start.into()],
15729        });
15730
15731        // Enter sketch edit mode.
15732        frontend
15733            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15734            .await
15735            .unwrap();
15736        let (src_delta, _scene_delta) = frontend
15737            .add_constraint(&mock_ctx, version, sketch_id, constraint)
15738            .await
15739            .unwrap();
15740        assert!(
15741            src_delta.text.contains("coincident("),
15742            "Expected coincident constraint in source, got: {}",
15743            src_delta.text
15744        );
15745
15746        ctx.close().await;
15747        mock_ctx.close().await;
15748    }
15749
15750    #[tokio::test(flavor = "multi_thread")]
15751    async fn test_extra_newlines_add_line_then_edit_line() {
15752        // Extra newlines after @settings - add a line, then edit it.
15753        let initial_source = "@settings(defaultLengthUnit = mm)
15754
15755sketch001 = sketch(on = XY) {
15756}
15757";
15758
15759        let program = Program::parse(initial_source).unwrap().0.unwrap();
15760        let mut frontend = FrontendState::new();
15761
15762        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15763        let mock_ctx = ExecutorContext::new_mock(None).await;
15764        let version = Version(0);
15765
15766        frontend.hack_set_program(&ctx, program).await.unwrap();
15767        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15768        let sketch_id = sketch_object.id;
15769
15770        // Add a line.
15771        let line_ctor = LineCtor {
15772            start: Point2d {
15773                x: Expr::Number(Number {
15774                    value: 0.0,
15775                    units: NumericSuffix::Mm,
15776                }),
15777                y: Expr::Number(Number {
15778                    value: 0.0,
15779                    units: NumericSuffix::Mm,
15780                }),
15781            },
15782            end: Point2d {
15783                x: Expr::Number(Number {
15784                    value: 10.0,
15785                    units: NumericSuffix::Mm,
15786                }),
15787                y: Expr::Number(Number {
15788                    value: 10.0,
15789                    units: NumericSuffix::Mm,
15790                }),
15791            },
15792            construction: None,
15793        };
15794        let segment = SegmentCtor::Line(line_ctor);
15795        let (src_delta, scene_delta) = frontend
15796            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15797            .await
15798            .unwrap();
15799        assert!(
15800            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15801            "Expected line in source after add, got: {}",
15802            src_delta.text
15803        );
15804        // Line creates start point, end point, and line segment.
15805        let line_id = *scene_delta.new_objects.last().unwrap();
15806
15807        // Edit the line.
15808        let line_ctor = LineCtor {
15809            start: Point2d {
15810                x: Expr::Number(Number {
15811                    value: 1.0,
15812                    units: NumericSuffix::Mm,
15813                }),
15814                y: Expr::Number(Number {
15815                    value: 2.0,
15816                    units: NumericSuffix::Mm,
15817                }),
15818            },
15819            end: Point2d {
15820                x: Expr::Number(Number {
15821                    value: 13.0,
15822                    units: NumericSuffix::Mm,
15823                }),
15824                y: Expr::Number(Number {
15825                    value: 14.0,
15826                    units: NumericSuffix::Mm,
15827                }),
15828            },
15829            construction: None,
15830        };
15831        let segments = vec![ExistingSegmentCtor {
15832            id: line_id,
15833            ctor: SegmentCtor::Line(line_ctor),
15834        }];
15835        let (src_delta, scene_delta) = frontend
15836            .edit_segments(&mock_ctx, version, sketch_id, segments)
15837            .await
15838            .unwrap();
15839        assert!(
15840            src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
15841            "Expected edited line in source, got: {}",
15842            src_delta.text
15843        );
15844        assert_eq!(scene_delta.new_objects, vec![]);
15845
15846        ctx.close().await;
15847        mock_ctx.close().await;
15848    }
15849
15850    #[test]
15851    fn test_add_variable_declaration_uses_top_level_scope_after_sketch_block() {
15852        // A non-target sketch block appears before the target so that the
15853        // traversal enters and leaves it before reaching the target. The
15854        // generated name must come from the top-level scope, where foo1 is
15855        // taken, not the sketch's scope, where no foo names are taken. This
15856        // is a regression test: dfs_mut used to visit the sketch block twice,
15857        // pushing its scope twice but popping it once, leaving the sketch
15858        // scope on top of the defined-names stack for the rest of the
15859        // traversal.
15860        let code = "\
15861foo1 = 1
15862sk = sketch() {
15863  p = var 1.5
15864}
158657 + 8
15866";
15867        let mut ast = crate::parsing::top_level_parse(code).unwrap();
15868        let ast::BodyItem::ExpressionStatement(stmt) = &ast.body[2] else {
15869            panic!("expected an expression statement");
15870        };
15871        let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
15872        let (_, cmd_return) = mutate_ast_node_by_source_ref(
15873            &mut ast,
15874            &source_ref,
15875            AstMutateCommand::AddVariableDeclaration {
15876                prefix: "foo".to_owned(),
15877            },
15878        )
15879        .unwrap();
15880        let AstMutateCommandReturn::Name(name) = cmd_return else {
15881            panic!("expected a generated name");
15882        };
15883        assert_eq!(name, "foo2");
15884        let ast::BodyItem::VariableDeclaration(decl) = &ast.body[2] else {
15885            panic!("expected the expression statement to become a variable declaration");
15886        };
15887        assert_eq!(decl.name(), "foo2");
15888    }
15889
15890    /// Get the function body of the variable declaration at `ast.body[index]`.
15891    fn function_body_at(ast: &ast::Node<ast::Program>, index: usize) -> &ast::Node<ast::Program> {
15892        let ast::BodyItem::VariableDeclaration(decl) = &ast.body[index] else {
15893            panic!("expected a variable declaration");
15894        };
15895        let ast::Expr::FunctionExpression(func) = &decl.declaration.init else {
15896            panic!("expected a function expression");
15897        };
15898        &func.body
15899    }
15900
15901    /// Get the then-branch block of the if-expression initializing the
15902    /// variable declaration at `ast.body[index]`.
15903    fn then_block_at(ast: &ast::Node<ast::Program>, index: usize) -> &ast::Node<ast::Program> {
15904        let ast::BodyItem::VariableDeclaration(decl) = &ast.body[index] else {
15905            panic!("expected a variable declaration");
15906        };
15907        let ast::Expr::IfExpression(if_expr) = &decl.declaration.init else {
15908            panic!("expected an if expression");
15909        };
15910        &if_expr.then_val
15911    }
15912
15913    #[test]
15914    fn test_add_variable_declaration_in_function_body_uses_function_scope() {
15915        // The generated name must come from the function body's scope, where
15916        // thing1 is taken. Before dfs_mut visited function bodies as program
15917        // nodes, the top-level scope was used instead, generating thing1 and
15918        // colliding with the local.
15919        let code = "\
15920fn build() {
15921  thing1 = 1
15922  10 + 20
15923  return thing1
15924}
15925";
15926        let mut ast = crate::parsing::top_level_parse(code).unwrap();
15927        let ast::BodyItem::ExpressionStatement(stmt) = &function_body_at(&ast, 0).body[1] else {
15928            panic!("expected an expression statement");
15929        };
15930        let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
15931        let (_, cmd_return) = mutate_ast_node_by_source_ref(
15932            &mut ast,
15933            &source_ref,
15934            AstMutateCommand::AddVariableDeclaration {
15935                prefix: "thing".to_owned(),
15936            },
15937        )
15938        .unwrap();
15939        let AstMutateCommandReturn::Name(name) = cmd_return else {
15940            panic!("expected a generated name");
15941        };
15942        assert_eq!(name, "thing2");
15943        let body = &function_body_at(&ast, 0).body;
15944        assert_eq!(body.len(), 3);
15945        let ast::BodyItem::VariableDeclaration(decl) = &body[1] else {
15946            panic!("expected the expression statement to become a variable declaration");
15947        };
15948        assert_eq!(decl.name(), "thing2");
15949        // Siblings are untouched.
15950        let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
15951            panic!("expected a variable declaration");
15952        };
15953        assert_eq!(first.name(), "thing1");
15954        assert!(matches!(&body[2], ast::BodyItem::ReturnStatement(_)));
15955    }
15956
15957    #[test]
15958    fn test_delete_node_in_function_body_preserves_leading_comment() {
15959        // Before the shared body traversal, MutateBodyItem::Delete was
15960        // silently dropped inside function bodies, so this reported success
15961        // without deleting anything.
15962        let code = "\
15963fn build() {
15964  a = 1
15965  // keep me
15966  b = 2
15967  return a
15968}
15969";
15970        let mut ast = crate::parsing::top_level_parse(code).unwrap();
15971        let ast::BodyItem::VariableDeclaration(b_decl) = &function_body_at(&ast, 0).body[1] else {
15972            panic!("expected a variable declaration");
15973        };
15974        assert_eq!(b_decl.name(), "b");
15975        let source_ref = SourceRef::new(SourceRange::from(&b_decl.declaration.init), None);
15976        mutate_ast_node_by_source_ref(&mut ast, &source_ref, AstMutateCommand::DeleteNode).unwrap();
15977        let body = &function_body_at(&ast, 0).body;
15978        assert_eq!(body.len(), 2, "expected b to be deleted");
15979        let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
15980            panic!("expected a variable declaration");
15981        };
15982        assert_eq!(first.name(), "a");
15983        let ast::BodyItem::ReturnStatement(_) = &body[1] else {
15984            panic!("expected the return statement to remain");
15985        };
15986        assert!(
15987            body[1].get_comments().iter().any(|c| c.contains("keep me")),
15988            "expected the deleted item's leading comment to migrate to the next item, got: {:?}",
15989            body[1].get_comments()
15990        );
15991    }
15992
15993    #[test]
15994    fn test_add_variable_declaration_in_function_body_ignores_parameters() {
15995        // Locals in the function body are avoided: thing1 is taken, so the
15996        // generated name is thing2. But find_defined_names only sees the
15997        // block's body items, not the function's parameters, so the generated
15998        // name collides with the thing2 parameter. This pins the current
15999        // behavior.
16000        // TODO: Should function parameters be included in the scope used for
16001        // name generation?
16002        let code = "\
16003fn build(thing2) {
16004  thing1 = 1
16005  10 + 20
16006  return thing1 + thing2
16007}
16008";
16009        let mut ast = crate::parsing::top_level_parse(code).unwrap();
16010        let ast::BodyItem::ExpressionStatement(stmt) = &function_body_at(&ast, 0).body[1] else {
16011            panic!("expected an expression statement");
16012        };
16013        let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
16014        let (_, cmd_return) = mutate_ast_node_by_source_ref(
16015            &mut ast,
16016            &source_ref,
16017            AstMutateCommand::AddVariableDeclaration {
16018                prefix: "thing".to_owned(),
16019            },
16020        )
16021        .unwrap();
16022        let AstMutateCommandReturn::Name(name) = cmd_return else {
16023            panic!("expected a generated name");
16024        };
16025        assert_eq!(name, "thing2", "locals are avoided, but parameters are not");
16026    }
16027
16028    #[test]
16029    fn test_add_variable_declaration_in_if_branch_uses_branch_scope() {
16030        let code = "\
16031x = 1
16032y = if x > 0 {
16033  q1 = 1
16034  foo(q1)
16035  q1
16036} else {
16037  2
16038}
16039";
16040        let mut ast = crate::parsing::top_level_parse(code).unwrap();
16041        let ast::BodyItem::ExpressionStatement(stmt) = &then_block_at(&ast, 1).body[1] else {
16042            panic!("expected an expression statement");
16043        };
16044        let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
16045        let (_, cmd_return) = mutate_ast_node_by_source_ref(
16046            &mut ast,
16047            &source_ref,
16048            AstMutateCommand::AddVariableDeclaration { prefix: "q".to_owned() },
16049        )
16050        .unwrap();
16051        let AstMutateCommandReturn::Name(name) = cmd_return else {
16052            panic!("expected a generated name");
16053        };
16054        assert_eq!(name, "q2");
16055        let body = &then_block_at(&ast, 1).body;
16056        assert_eq!(body.len(), 3);
16057        let ast::BodyItem::VariableDeclaration(decl) = &body[1] else {
16058            panic!("expected the expression statement to become a variable declaration");
16059        };
16060        assert_eq!(decl.name(), "q2");
16061        // Siblings are untouched.
16062        let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16063            panic!("expected a variable declaration");
16064        };
16065        assert_eq!(first.name(), "q1");
16066        assert!(matches!(&body[2], ast::BodyItem::ExpressionStatement(_)));
16067    }
16068
16069    #[test]
16070    fn test_delete_node_in_if_branch_preserves_leading_comment() {
16071        // Before the shared body traversal, MutateBodyItem::Delete was
16072        // silently dropped inside if-expression branch blocks.
16073        let code = "\
16074y = if true {
16075  a = 1
16076  // keep me
16077  b = 2
16078  a + b
16079} else {
16080  2
16081}
16082";
16083        let mut ast = crate::parsing::top_level_parse(code).unwrap();
16084        let ast::BodyItem::VariableDeclaration(b_decl) = &then_block_at(&ast, 0).body[1] else {
16085            panic!("expected a variable declaration");
16086        };
16087        assert_eq!(b_decl.name(), "b");
16088        let source_ref = SourceRef::new(SourceRange::from(&b_decl.declaration.init), None);
16089        mutate_ast_node_by_source_ref(&mut ast, &source_ref, AstMutateCommand::DeleteNode).unwrap();
16090        let body = &then_block_at(&ast, 0).body;
16091        assert_eq!(body.len(), 2, "expected b to be deleted");
16092        let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16093            panic!("expected a variable declaration");
16094        };
16095        assert_eq!(first.name(), "a");
16096        let ast::BodyItem::ExpressionStatement(_) = &body[1] else {
16097            panic!("expected the tail expression to remain");
16098        };
16099        assert!(
16100            body[1].get_comments().iter().any(|c| c.contains("keep me")),
16101            "expected the deleted item's leading comment to migrate to the next item, got: {:?}",
16102            body[1].get_comments()
16103        );
16104    }
16105}